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

6.5

Loading...

Getting Started

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

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

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

Loading...

Appmixer UI SDK

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Customizing Embedded UI

Loading...

Loading...

Loading...

Appmixer Backoffice

Loading...

Loading...

Loading...

icon

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

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

Introduction

Appmixer is a white-label AI workflow automation platform built for SaaS vendors. It lets you embed a fully native AI workflow automation hub directly into your product — so your users can automate workflows, connect third-party apps, and run AI agents, all without leaving your platform.

Appmixer is available as a cloud solution or as a self-hosted deployment, giving you full control over your data and infrastructure.

Start by building some automation templates for your Automation Hub.

Build Your Automation Hub

The Automation Hub is a one-stop shop for automations and AI agents for your end users.

Get started with the Automation Hub:

  1. Build and Publish a Template

  2. Customize and Share Your Hub

  3. Embed Your Automation Hub


Embed your own Hub to allow users to:

  • Browse and start Automations pre-built by your team

  • Customize pre-built Automations

  • Manage Logs and Accounts for their Automations

Your team can easily manage the Hub:

  • Organize automations into categories (shown as tabs in the Hub)

  • Adjust the look and feel of the Hub

  • Monitor health and statistics of Hub Automations

  • Define who can access specific automations

End User Guide

Please visit the for end-user tutorials.

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.

Customize and Share Your Hub

All published templates are available in the Automation Hub - an all-in-one embeddable widget that you can use to present automations to your end users.

The Automation Hub provides these features to your users out of the box:

  • Starting and managing automations including logs and accounts

  • Searching and filtering

  • Customization of pre-built automations (you can choose to enable or disable this option)

aiDescription

Optional. A description of your component written for the AI Copilot rather than for the Designer UI. Copilot's component discovery prefers aiDescription and falls back to , so a connector that does not define it behaves exactly as before.

Use it where the UI description is too short to steer the model — say what the component is for, and the nearest thing it is not for:

aiDescription may be at most 1000 characters. It is read on every component-discovery call, which lists every component of an app at once, so keep it to a few sentences.

The field is also accepted in module and service manifests.

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.

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.

description

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

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

Allow or Disable customization of pre-built templates by end-users

Monitor Hub Performance

Knowledge base

Appmixer Knowledge base
Component Behaviour
{ "label": "Create Tweet" }
{
    "description": "Creates a customer in Salesforce.",
    "aiDescription": "Use this component when the user wants to create a new Salesforce Account or Contact. It does not update existing records."
}
description
{ "name": "appmixer.twitter.statuses.CreateTweet" }

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ář <martin@client.io>",
    "label": "On Flow Start",
    "description": "This trigger fires once and only once the flow starts.",
    "icon": "data:image/svg+xml;base64,PD94bWwgdmV...",
    "outPorts": [
        {
            "name": "out",
            "schema": {
                "properties": {
                    "started": {
                        "type": "string",
                        "format": "date-time"
                    }
                },
                "required": [ "started" ]
            },
            "options": [
                { "label": "Start time", "value": "started" }
            ]
        }
    ]
}
Use the options in the Automation Hub Settings section to adjust the page structure and design.

Use the options on the page to adjust:

  • Header and subheader (show/hide and text)

  • Default layout (grid or list)

  • Show/hide Logs and Connections tabs

Automations can be organized into tabs for easier access to specific types of automations such as CRM, Notifications, AI or other.

To organize and assign categories:

  1. Assign a category to a template in the Templates section: Select the Manage categories option in the context menu of a template in the Templates section. Select an existing category or add a new one.

  2. Define tab name and select a category in the Tabs section: Enter the name and select one of the available categories in the dropdown

Use the Theme Options section to customize colors and fonts.

Use these options to define how much customization is available to your end-users.

Customization of automations in the Hub has two levels.

Users can see the "backend" of an automation and make changes to the nodes on the editor canvas.

Available in the context menu of each automation as Customize in Editor

Users can build completely new automations by choosing the nodes themselves on the editor canvas.

Available as the Create my own button in the Hub.

You can easily share the Hub with others so that they can try and deploy pre-built automations even without signing up for the Appmixer Studio.

To share access to the Hub:

  1. Add their email address in the Sharing Options section in the Hub Settings

  2. Click the Share Hub button to copy a shareable link

  3. Once they verify their email, they will get full access to the Hub

Use the Preview button on the Dashboard, in the Templates section or in the Hub Settings to check how the Automation Hub looks.

Design and Feature Customizations

Text and toggles in the Hub

Organizing Automations into Categories

Theme

Feature Customizations

Allow customization of pre-built automations

Allow creating custom automations from scratch

All the customization options are reflected in the Embed your Hub section on the page, so that you can explore the embed code before starting your implementation.

Sharing the Hub

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

Monitor & Troubleshoot

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

Look up your users

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

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.

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.

Public Files

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

Returns a list of the public files

GET [TENANT_API_URL]/public-files

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

[
  {
    "filename"

Upload a public file

POST [TENANT_API_URL]/public-files

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

Request Body

Name
Type
Description

DELETE [TENANT_API_URL]/public-files/:filename

Name
Type
Description

auth

The authentication service and parameters. For example:

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

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

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

Connected Accounts

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.

Flow

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

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

Delayed Loop

Send the items of a list one after another with a pause between them, for example to stay under an API rate limit.

Introduction

By default the Each component sends all the items of a list at once. The steps in the loop body then call the target API for all the items at the same time, which can hit its rate limit.

Set Delay on Each to space the items out. Each then sends one item, waits the given number of milliseconds, sends the next one, and so on.

Each with a delay of 500 ms between items

Settings

  • Delay: the pause between two items in milliseconds, from 0 to 60000 (one minute). 0 or empty means no delay.

For example, an API that allows 10 requests per second needs a delay of at least 100 ms.

How it works

  • Each sends the items in their list order, with the pause between them. The item messages look the same as in the default mode: Index, Value, Items Count and Correlation ID.

  • done is sent once the last item has been sent.

  • A long list is processed in batches of up to three minutes in the background, so a delayed loop can run for a long time without blocking the flow. If the engine delivers the input message again after an error, the loop continues from the last sent item instead of starting over.

  • Join Each works the same as in a parallel loop, see .

Monitor Hub Performance

The Analytics dashboard gives you a real-time view of how your end users are engaging with your Automation Hub - what's running, what's being adopted, and where issues are occurring.

Access Analytics from the Automation Hub section in the main navigation menu.


What you'll find

Summary metrics

Four tiles at the top of the page give you an at-a-glance health check of your Hub:

  • Total Running Instances — the number of active integration instances currently running across all your end users

  • Active Users (Last 30d) — how many users have been active in the past 30 days, with a percentage change compared to the previous period

  • Data Messages (Last 30d) — total data messages processed, with a percentage change compared to the previous period

  • Success Rate (Last 30d) — the share of flow runs that completed without errors

Template lists

Below the summary tiles, four lists give you a deeper breakdown by template:

  • Most Popular Templates — ranked by number of active instances

  • Most Active Templates — ranked by volume of data messages processed

  • Template Adoption — ranked by new activations in the last 30 days

  • Success Rate — ranked by the percentage of successful (error-free) runs

Each list defaults to highest-to-lowest order. Click the sort icon to toggle the direction — useful when you want to surface your least-adopted or most error-prone integrations.


Spot adoption gaps. The Template Adoption list shows which integrations your end users are actually activating. If a template you've invested in isn't being picked up, that's a signal to revisit its description, positioning, or discoverability in the Hub.

Find reliability problems early. The Success Rate list is your fastest path to identifying broken or underperforming integrations. Sort lowest-to-highest to surface the most problematic ones and investigate via Logs.

Understand engagement trends. Active Users and Data Messages both include a 30-day delta, so you can see whether engagement is growing or declining without running any additional queries.


All metrics shown on this page are also available via the Appmixer REST API. If you're building custom dashboards or integrating Hub analytics into your own admin tooling, see the Analytics API reference for available endpoints.

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. Throughout this documentation, it is referred to as [TENANT_API_URL]. Replace it with the API URL of your tenant, for example:

  • https://api-acme.appmixer.ai for newer hosted tenants,

  • https://api.acme.appmixer.cloud for older hosted tenants,

  • the API URL of your own installation in case of a Self-Managed Appmixer.

Similarly, [TENANT_STUDIO_URL] refers to the URL of your Appmixer Studio, for example https://acme.appmixer.ai or https://my.acme.appmixer.cloud, and [TENANT_BACKOFFICE_URL] to the URL of your Appmixer Backoffice, for example https://backoffice-acme.appmixer.ai or https://backoffice.acme.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:

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

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 .

The REST API is also described by an specification. Import it into tools such as Postman, Insomnia or Swagger UI to explore the API, generate API clients in your programming language, or give AI agents a machine-readable description of the API. Set the tenantApiUrl server variable to your [TENANT_API_URL].

Embed Your Automation Hub

The Automation Hub embeddable widget combines these features for your end-users:

  • Browse and start Automations pre-built by your team

  • Customize pre-built Automations

  • Manage Logs and Accounts for their Automations

As you customize the Hub options such as layout, design and automation categories (tabs), the embed code shown in the Settings page will dynamically change.

You can copy the code from the Settings page or use this example code:

in the dedicated documentation section.

Config

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

GET [TENANT_API_URL]/config

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

POST [TENANT_API_URL]/config

Build and Publish a Template

This tutorial will guide you through the process of creating, testing, and publishing your first Automation Template:

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

  • Test the Automation Template

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:

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.

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.

private

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

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

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.

A delay controls when an item is sent, not when it is finished. If one item takes longer in the loop body than the delay, the next item starts before the previous one is done, and the items can still finish out of order. When every item has to be finished before the next one starts, use a sequential loop.

Parallel loop with Join Each

How to use this

Access analytics via API

Name
Type
Description

key*

String

Configuration key

value*

Any

Configuration value

DELETE [TENANT_API_URL]/config/:key

Name
Type
Description

key*

String

The key of the configuration to be removed

Get all configuration entries

Create a configuration key/value pair

Request Body

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

Removes a configuration entry

Path Parameters

:
"
test.txt
"
}
]

filename*

String

The name for the file

file*

File

The file to be uploaded

filename*

String

The name of the file you want to remove

Removes a public file

Path Parameters

{ "ok": true }

OpenAPI Specification

here
OpenAPI 3.1
appmixer-openapi.yaml
311KB
Open

Authenticating your end users

Read more about authenticating your users

Dependencies

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

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

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

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

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

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:

{
    "name": "appmixer.utils.http.WebhookWithOPTIONS",
    "description": "Support for OPTIONS and POST",
    "webhook": true,
    "httpRequestMethods": [ "POST", "OPTIONS" ], 
    ...
}

author

The author of the component. Example:

{
    "author": "David Durman <david@client.io>"
}
{
  "key": "myConfigKey",
  "value": "My Custom Value"
}
{ "ok": true }
{ "ok": true }
$ curl -XPOST "[TENANT_API_URL]/user/auth" \
-H "Content-type: application/json" \
-d '{ "username": "abc@example.com", "password": "abc321" }'
{
    "user": {
        "id": "5c88c7cc04a917256c726c3d",
        "username":"abc@example.com",
        "email": "abc@example.com"
    },
    "token":"eyJhbGciOiJIUzI1NiIsInR5cC..."
}
$ curl "[TENANT_API_URL]/flows" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cC..."
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Appmixer Automation Hub – Minimal Demo</title>
</head>
<body>
  <div id="widget"></div>

  <script src="[TENANT_STUDIO_URL]/appmixer/package/appmixer.js"></script>

  <script type="module">
    const API_BASE_URL = '[TENANT_API_URL]';
    const USERNAME = 'YOUR_USERNAME';
    const PASSWORD = 'YOUR_PASSWORD';

    const appmixer = new Appmixer({ baseUrl: API_BASE_URL, debug: true });
    const { token } = await appmixer.api.authenticateUser(USERNAME, PASSWORD);
    appmixer.set('accessToken', token);

    appmixer.ui.AutomationHub({
    el: '#widget',
    state: {
        flows: {
            layout: 'grid'
        }
    },
    options: {
        customization: {
            entryPoints: {
                templates: true,
                scratch: false
            }
        },
        header: {
            visible: true,
            tabs: {
                hidden: []
            },
            subheader: {
                visible: false
            }
        },
        flows: {
            header: {
                layout: {
                    visible: true
                }
            },
            templates: {
                header: {
                    categories: {
                        visible: false,
                        tabs: []
                    }
                }
            }
        }
    },
    l10n: {
        ui: {
            automationHub: {}
        }
    },
    theme: {
        mode: 'dark',
        variables: {
            colors: {
                surface: '#2A2A2A',
                neutral: '#FFFFFF',
                primary: '#2B75EF',
                onPrimary: '#FFFFFF',
                secondary: '#94A6D4',
                onSecondary: '#FFFFFF',
                tertiary: '#D494D0',
                onTetriary: '#FFFFFF',
                error: '#EF4444',
                warning: '#F6C20C',
                onWarning: '#FFFFFF',
                success: '#01C58D',
                onSuccess: '#FFFFFF',
                modifier: '#C558CF',
                onModifier: '#FFFFFF',
                highlighter: '#FFA500',
                separator: '#4C4C4C',
                charcoalTeal: '#2C3130',
                darkJade: '#2C4B42'
            },
            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,
                size: 14
            },
            shadows: {
                level0: 'none',
                level1: 'none',
                level2: 'none',
                level3: 'none',
                level4: 'none',
                level5: 'none',
                backdrop: 'rgba(0 0 0 / 92%)',
                popover: '1px 3px 9px rgba(0 0 0 / 32%)',
                icon: 'none',
                blur: 'rgba(0 0 0 / 75%)',
                bar: 'none'
            }
        }
    }
}).open();
  </script>
</body>
</html>

Publish your Template to your end-users for immediate use.

  • See a preview of how your template will be shown to your end-users

  • The Get Started section on the Dashboard of Appmixer Studio provides examples of automation templates. Each one has built-in notes describing how to use it

    This tutorial explains how to build one of the demo templates from scratch, specifically the one called Get Slack Notifications for Hot Leads.

    This template 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 automation template examines each contact for the presence of a hotLead parameter. If this parameter is set to true, the contact is then forwarded to Slack.

    From the Templates page in the Build section, click the "New Template" button located in the top right corner. Then, select the OnAppEvent trigger from the Trigger selector. Utilizing App Events 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 template 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 template 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:

    The next step is to setup the Slack - Send Channel Message component. This component includes three configuration fields:

    • Slack account

    • Slack channel

    • 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 Add to template checkbox 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 as seen on the screenshot above.

    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 automation template 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 automation template:

    At this point, our automation template is ready - let's check the appearance of the final configuration web form (Wizard) as it will be presented to our end-users.

    Click Preview & Adjust to open the Wizard Builder dialog to:

    • Add and reorder fields

    • Setup custom labels, tooltips and placeholders

    • Set advanced options (more details below)

    • See a live preview of the wizard

    For each field, you can set advanced options. For account/authentication fields, you can also choose to share an account with the end-users of the template.

    Use this option to share your account with the end-users of this integration template. This means that end-users will not connect their own accounts, but use the shared account used by the author of the template.

    Use this option to completely hide the tooltip for a field in the wizard UI.

    Use this option to prevent end users from adding data variables in a field.

    Use this option to define which components within the template will be available as a source of variables within that field, i.e. only allow users to map variables from a specific component(s).

    Use this option to prevent users from applying data transformations to variables within that field.

    For our template, you can for example rename the default "Channel" and "Message" fields within the wizard to "Slack Channel" and "Slack Message," respectively.

    Next, proceed with a live test of the automation template. Click the "Start Test" button at the top of the page to initiate the template 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 of the automation template 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 automation. 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 template by navigating to "Test" and then selecting "Insights" from the menu:

    Now that your template is ready and has been thoroughly tested, you are all set to publish it for your end-users. Click Publish on the right side of the upper toolbar.

    You can publish a template to all your users, or uncheck the All users option and choose from these options:

    • User

    • Scope

    • Domain

    Check this option to push your update to existing instances, i.e. end-user automations created from your template. For this option, you can also set if in-progress data should be kept or deleted for running instances.

    Please note, if Appmixer has not yet been embedded into your product, you can easily open a preview of the Automation Hub by clicking the Preview button in the Templates section or on the Dashboard.

    Explore our demo content or build from scratch

    Template Overview - Slack Notifications for Hot Leads

    Create an Automation Template

    You can also pick ready-to-use templates from our Get Started section on the Dashboard

    Select Configuration Fields to Collect from your End-Users

    Predefine values for your users

    Apply data transformations

    Refine the template wizard

    Advanced wizard options

    Share account with all template users

    Hide tooltip

    Disable variables

    Restrict variables

    Disable modifiers

    Test your Template

    Publish your Template to your End-Users

    Set target users

    Update existing instances

    Please note that it is not possible to force updates that would touch end-user configuration, i.e. values and variables in configuration fields.

    Preview the Template

    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.

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

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

    Alternatively, you can trigger the OnAppEvent from either client-side or backend-side code by sending an HTTP POST request to the endpoint [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 Sign-in endpoint. 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.

    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.

    Triggering OnAppEvent using Appmixer SDK

    appmixer.api.sendAppEvent('contact-created', {
        email: 'david@example.com',
        fname: 'David',
        lname: 'Doe'
    });
    curl -XPOST \
               -H 'Content-Type: application/json' \
               -H "Authorization: Bearer VIRTUAL_USER_ACCESS_TOKEN" \
               -d '{ "email": "david@example.com", "fname": "David", "lname": "Doe" }' \
               "[TENANT_API_URL]/plugins/appmixer/utils/appevents/events/contact-created"

    Triggering OnAppEvent using HTTP requests

    Inspector panel

    quota

    Configuration of the quota manager used for this component. Quotas allow you to throttle the firing of your component. This is especially useful and many times even necessary to make sure you don't go over 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:

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

    Dynamic values

    The {{}} can be used in any property within the quota definition. And values from two objects - the user's metadata, and the account's profileInfo can be used there. The following example shows how to dynamically select a resource based on the value of the user's metadata.tier .

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

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

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

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

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

    quota.manager

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

    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.

    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.

    Storage

    Manage records associated with data storage utility components of flows.

    Storage

    Configuration

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

    const storage = appmixer.ui.Storage(config)
    
    storage.set(key, value)
    storage.get(key)

    config.el ...

    Learn about widget config .

    config.storeId

    Type: String | Default: []

    ID of a store to open within the storage.

    Instance

    Learn about widget instance .

    State

    storage.state(name, value)

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    Type: String | Default: null

    Toggle a custom error message.

    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.

    POST [TENANT_API_URL]/user/auth

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

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

    Flow

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

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

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

    People Tasks

    Manage tasks created by utility components of flows.

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

    Type: Boolean | Default: null

    Toggle a custom loading state.

    Type: String | Default: null

    Accounts

    Manage accounts authorized by the current user.

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

    Type: Boolean | Default: null

    Toggle a custom loading state.

    Type: String | Default: null

    Installation

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

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

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

    Choose Appmixer UI widgets to include:

    Quick Start

    Refer to the getting started guide to get started quickly.

    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:

    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

    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:

    quota.resources

    quota.scope

    Embed into Your Application

    Basic Usage

    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 the hosted [TENANT_STUDIO_URL], you will reference your own Studio URL). Alternatively, you can download the appmixer.js file and link to it from whatever location you will put it in.

    Advanced Usage

    https://github.com/clientIO/appmixer-demo-embedded-integrations
    https://github.com/clientIO/appmixer-demo-firebase-vanilla
    <script src="[TENANT_STUDIO_URL]/appmixer/appmixer.js"></script>
    
    <script type="module">
    const appmixer = new Appmixer({ baseUrl: '[TENANT_API_URL]' })
    appmixer.api.authenticateUser(username, password).then(auth => {
        appmixer.set('accessToken', auth.token);
        ...
        const integrations = new appmixer.ui.Integrations({ el: '#integrations' });
        integrations.open();
    });
    </script>
    wget [TENANT_STUDIO_URL]/appmixer/package/appmixer.es.js
    wget [TENANT_STUDIO_URL]/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(/* ... */)
    password
    .
    Name
    Type
    Description

    password*

    string

    Password.

    username

    string

    {
        "user": {
    

    POST [TENANT_API_URL]/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": "abc@example.com", "email": "abc@example.com", "password": "abc321" }'

    Name
    Type
    Description

    password*

    string

    Password.

    email*

    string

    {
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
    }
    {
        "error": "Too many signup attempts for this email address. Please try again later."
    }

    or

    {
        "error": "Too many signup attempts from your IP address. Please try again later."
    }

    To protect against abuse, this endpoint implements rate limiting for unauthenticated signup requests (public user registration). Rate limiting applies two layers of protection:

    1. Email-based rate limiting: Limits signup attempts per email address

    2. IP-based rate limiting: Limits signup attempts per IP address

    Rate limits can be configured using environment variables:

    Email-based limits:

    • USER_SIGNUP_RATE_LIMIT_EMAIL - Maximum signups per email (default: 10)

    • USER_SIGNUP_RATE_LIMIT_EMAIL_WINDOW_MS - Time window in milliseconds (default: 3600000 = 1 hour)

    IP-based limits:

    • USER_SIGNUP_RATE_LIMIT_IP - Maximum signups per IP address (default: 50)

    • USER_SIGNUP_RATE_LIMIT_IP_WINDOW_MS - Time window in milliseconds (default: 3600000 = 1 hour)

    See User Signup Rate Limiting in the configuration guide for more details.

    When a rate limit is exceeded, the API returns:

    • HTTP Status: 429 Too Many Requests

    • Error Message: Descriptive message indicating which limit was exceeded

    Example responses:

    • Email limit: "Too many signup attempts for this email address. Please try again later."

    • IP limit: "Too many signup attempts from your IP address. Please try again later."

    GET [TENANT_API_URL]/user

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

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

    Sign-in User

    Request Body

    Create User

    Request Body

    Rate Limiting

    Rate limiting only applies to unauthenticated signups. When an authenticated admin creates a user via the API, rate limiting is bypassed.

    Configuration

    Error Handling

    For bulk user creation or migrations, use an authenticated admin account to create users. This bypasses rate limiting entirely.

    Get User Information

    Toggle a custom error message.

    const peopleTasks = appmixer.ui.PeopleTasks(config)
    
    peopleTasks.set(key, value)
    peopleTasks.get(key)
    peopleTasks.state(name, value)

    Configuration

    config.el ...

    Learn about widget config here.

    Instance

    Learn about widget instance here.

    State

    loader

    error

    People Tasks
    const peopleTasks = appmixer.ui.PeopleTasks({
        el: '#people-tasks'
    })
    
    peopleTasks.open()

    Example

    Toggle a custom error message.

    Select a flow to open in Designer widget.

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

    Configuration

    config.el ...

    Learn about widget config here.

    Instance

    Learn about widget instance here.

    State

    loader

    error

    Accounts
    accounts.on(event, handler)
    accounts.on('flow:open', flowId => {/* ... */})
    const accounts = appmixer.ui.Accounts({
        el: '#accounts'
    })
    
    accounts.open()

    Events

    flow:open

    Example

    const storage = appmixer.ui.Storage({
        el: '#storage'
    })
    
    storage.open()

    error

    Example

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

    Flow Descriptor

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

    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.

    You can also build a flow using the AI copilot. When you start building a new flow, you can choose between two options: add a trigger manually, or build with AI.

    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:

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

    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:

    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:

    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.

    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.

    The Designer renders flows with a redesigned diagram and a modern look, in both light and dark modes. If your canvas gets messy while building, click the Align Diagram button in the upper toolbar: the flow is re-arranged into a clean left-to-right layout and zoomed to fit. Only component positions change — connections and configuration stay untouched — and the alignment can be undone in a single step.

    Before starting your automation, you can test it using the "Test flow" button in the upper toolbar. This runs the flow once so you can verify that each component behaves as expected without needing to start it live.

    Once the test finishes, you can see the status of each component and preview its logs by hovering over the individual components on the canvas.

    The test status notification also tells you what kind of data was used for the test:

    • Real data from the trigger component, if it was available.

    • Historical data from the logs, if no real data was found on the trigger component.

    • Dummy data generated for the test, if neither real nor historical data was available.

    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.

    You can edit a running automation without stopping it. When an automation is active, an "Edit" button appears in the upper toolbar. Clicking it creates a draft, where you can make changes and test them just like on any other flow. Once you're happy with the draft, click "Publish changes" to replace the active version with it.

    In addition to editing via a draft, you can view and manage the active version, the draft, and other versions in the versioning panel. Open it by clicking the Versioning icon on the left side of the upper toolbar, next to the automation's name. From the versioning panel you can browse through previous versions, create new versions manually as snapshots of the current state, or restore a previous version.

    Connector Configuration

    Global configuration for your connectors. Values stored for a service are injected into the `context.config` object (alias `context.auth`) of every component of that service, and into the `context` of

    Only users with admin scope can use these endpoints.

    Get Services Configuration

    GET [TENANT_API_URL]/service-config

    Get a list of stored configurations.

    Query Parameters

    Name
    Type
    Description

    GET [TENANT_API_URL]/service-config/:serviceId

    Get the configuration stored for the given service.

    Name
    Type
    Description

    POST [TENANT_API_URL]/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" }

    Name
    Type
    Description

    PUT [TENANT_API_URL]/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.

    Name
    Type
    Description
    Name
    Type
    Description

    DELETE [TENANT_API_URL]/service-config/:serviceId

    Removes the configuration from the given service.

    Name
    Type
    Description

    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 Appmixer Backoffice interface:

    Custom OAuth Credentials

    For detailed information on configuring specific connectors, please consult the 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.

    A few connectors call third-party APIs with an API key provided by Appmixer, so your end-users can use them right away without connecting an account of their own. These keys are shared by every tenant, so the Appmixer engine enforces a usage limit on them. The limit applies per Appmixer tenant and is shared by all users and flows of that tenant.

    When a limit is reached, the engine does not fail the flow. The holds the message in a queue and processes it once the time window allows, so the flow only slows down. The third-party API is never called beyond the limit. The exception is the Email connector, where the Appmixer Cloud e-mail service rejects requests above its hourly limit and the component reports an error.

    If you need higher limits, you can switch any of these connectors to your own key. Create a Connector Configuration entry in the Backoffice (or via the ) with the Configuration ID and keys listed below. The connector picks the configuration up automatically, no re-installation is needed.

    • Email (appmixer.utils.email, SendEmail). By default e-mails are sent through Appmixer's Mailchimp Transactional (Mandrill) account from no-reply@appmixer.com. Limit: 100 e-mails per hour, and 700 e-mails per day. Above 100 per hour the component fails with a rate-limit error and the engine retries the message later. Your own key: Configuration ID appmixer:utils:email, key apiKey set to your Mailchimp Transactional API key, and optionally fromEmail for the sender address. With your own key the e-mails are sent directly through your Mailchimp account and the hourly limit no longer applies.

    • Weather

    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 ). Given that your Appmixer tenant operates under the domain of your [TENANT_API_URL] and you lack access to this domain's DNS settings, your options for domain verification can seem limited.

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

    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 [TENANT_API_URL]/google52658022a92d779c.html as well as at the well-known directory URL [TENANT_API_URL]/.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.

    ACL

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

    Get ACL types

    GET [TENANT_API_URL]/acl-types

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

    [
        "routes",
    
    

    Get ACL rules for components|routes

    GET [TENANT_API_URL]/acl/:type

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

    Name
    Type
    Description

    POST [TENANT_API_URL]/acl/:type

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

    Name
    Type
    Description
    Name
    Type
    Description

    GET [TENANT_API_URL]/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.

    Name
    Type
    Description

    GET [TENANT_API_URL]/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.

    Name
    Type
    Description

    GET [TENANT_API_URL]/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.

    Name
    Type
    Description

    firePatterns

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

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

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

    {
        "firePatterns": [1, 1]
    }

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

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

    The following table lists all the possible fire pattern symbols:

    Symbol
    Description

    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.

    Files

    Manage files for use with components of flows.

    Configuration

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

    const files = appmixer.ui.Files(config)
    
    files.set(key, value)
    files.get(key)

    config.el ...

    Learn about widget config .

    Instance

    Learn about widget instance .

    State

    files.state(name, value)

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message. &#xNAN;query

    Type: Object | Default: DefaultQuery

    Defines custom query parameters for retrieving files. Example:

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

    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.

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

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

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

    Basic Structure

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

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

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

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

    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

    Untitled

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

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

    Type: String | Default: null

    Base URL of your Appmixer engine REST API.

    Type: String | Default: null

    Wizard

    Manage a flow that is used as an integration instance.

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

    Type: String | Default: null

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

    Type: Boolean | Default: null

    Connectors

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

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

    Type: Boolean | Default: null

    Toggle a custom loading state.

    Type: String | Default: null

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

    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.

    Email address.

    username*

    string

    Username.

    pattern

    string

    A term to filter configurations containing pattern on their service id

    sort

    string

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

    offset

    number

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

    limit

    number

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

    string

    The service id. Example: appmixer:google

    whatever

    string

    Any value for the whatever-key

    serviceId

    string

    serviceId

    string

    The service id. Example

    whatever-key

    string

    Any value you need

    serviceId

    string

    The service id. Example: appmixer:google

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

    Get Service Configuration

    Path Parameters

    Create Service Configuration

    Request Body

    Update Service Configuration

    Path Parameters

    Request Body

    Delete Service Configuration

    Path Parameters

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

    "components"
    ]

    type

    string

    components | routes

    type

    string

    components | routes

    array

    Body has to be an array of ACL rules, where each rule has the following structure: { role: string - admin

    type

    string

    components | routes

    type

    string

    components | routes

    type

    string

    components | routes

    resource

    string

    Path Parameters

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

    Update ACL rules

    Path Parameters

    Request Body

    Get available resource values

    Path Parameters

    Get available action values

    Path Parameters

    Get available options for attributes property.

    Path Parameters

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

    Events

    flow:open

    Example

    here
    here

    Custom API

    Appmixer SDK allows you to override API methods used by the SDK instance. This can be handy in edge case scenarios where you need to override the API requests and their parameters or response values.

    Setting a custom API option

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

    var myCustomApi = {
      /* the key must match an existing API method */
      myCustomApiMethod(/* arguments of the original method */) {
        return new Promise((resolve) => {
          resolve(myCustomResponse);
        });
      }
    }
    
    /* Use a custom API on the entire SDK instance */
    var appmixer = new Appmixer({ api: myCustomApi });
    
    /* Use a custom API on a particular SDK UI widget */
    var designer = new appmixer.ui.Designer({ api: myCustomApi });

    The list of API methods can be found here.

    An example how to redefine the flow update request.

    Access token of an authorized user.

    Type: Boolean | Default: false

    Enable debugger for development purposes.

    Type: Object | Default: DefaultTheme

    Type: Object | Default: DefaultL10N

    Define custom localization texts. See Custom Strings for the structure of the object and how overrides are resolved.

    Type: String | Default: en

    Specify a language code for the localization of components.

    Type: Object | Default: {}

    Set custom API methods.

    Register and create UI Widgets.

    Use methods of built-in API Module.

    Set configuration property.

    Get configuration property.

    Register a custom Designer component shape.

    Register a custom Designer inspector field.

    Configuration

    baseUrl

    accessToken

    debug

    theme

    l10n

    lang

    api

    Instance

    appmixer.ui

    appmixer.api

    appmixer.set

    appmixer.get

    appmixer.registerCustomComponentShape

    appmixer.registerInspectorField

    Toggle a custom error message.

    Configuration

    config.el ...

    Learn about widget config here.

    Instance

    Learn about widget instance here.

    State

    loader

    error

    Example

    {
    	"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"
    }
    {}
    ['*', 'flows']
    ['*', 'read', '!read', 'create', '!create', 'update', '!update', 'delete', '!delete']
    // 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()
    /* 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
                });
            }
        }
    });
    const appmixer = new Appmixer({/* [name]: value */})
    
    appmixer.set(name, value)
    appmixer.get(name)
    appmixer.ui('Widget', {/* ... */})
    appmixer.ui.Widget({/* ... */})
    appmixer.set(key, value)
    appmixer.get(key, value)
    appmixer.registerCustomComponentShape(name, shape)
    appmixer.registerInspectorField(type, Field, options)
    const 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()
    (
    appmixer.utils.weather
    ). Uses OpenWeatherMap. Limit: 60 requests per minute. Your own key: Configuration ID
    appmixer:utils:weather
    , key
    apiKey
    set to your OpenWeatherMap API key.
  • Language (appmixer.utils.language). Uses Google Cloud Translation and Natural Language APIs. Limit: 10 000 requests per day. Your own key: Configuration ID appmixer:utils:language, keys clientEmail, privateKey and projectId of a Google Cloud service account with the Cloud Translation API and Cloud Natural Language API enabled.

  • AI utilities (appmixer.utils.ai, SendPrompt, GenerateImage, CreateSpeech, CreateTranscription, DescribeImages, Moderate, TransformTextToJSON). Uses OpenAI. No default key is provided and the engine enforces no limit; the limits of your OpenAI account apply. Configuration ID appmixer:utils:ai, key apiKey set to your OpenAI API key. You can also override the model per component with the keys SendPromptModel, GenerateImageModel, CreateSpeechModel, CreateTranscriptionModel, DescribeImagesModel, ModerateModel and TransformTextToJSONModel.

  • Azure Cognitive Services (appmixer.azureCognitiveServices). Limit: 20 requests per minute and 5 000 requests per month. Your own key: Configuration ID appmixer:azureCognitiveServices, keys key and endpoint of your Azure Computer Vision resource.

  • Screenshot API (appmixer.screenshotapi). Limit: 100 screenshots per month. Your own key: Configuration ID appmixer:screenshotapi, key apiToken set to your screenshotapi.net token.

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

    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.

    Connectors Using Appmixer-Managed API Keys

    The Configuration ID is derived from the connector name: appmixer.utils.weather reads appmixer:utils:weather, appmixer.screenshotapi reads appmixer:screenshotapi. Values are available to the connector's code in context.config (alias context.auth) regardless of whether the component declares an auth or authConfig section.

    Domain Verification

    quota manager
    Connector Configuration API
    Appmixer Backoffice
    Google OAuth app
    App Registration

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

    *

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

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

    A

    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.

    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.

    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 [TENANT_API_URL]/stats/component-usage?componentType=trello&stage=stopped&group=$flowId, and subsequently stop each flow individually with a POST request to [TENANT_API_URL]/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.

    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.

    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.

    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.

    Connector Details

    Installing a connector

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

    Access Control

    Removing a connector

    Installing Connectors from your filesystem

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

    Upgrading a Connector to a New Version

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

    Major Version Upgrade

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

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

    Available fields are:

    Field
    Description

    name

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

    label

    The label of your app.

    category

    Service manifest fields meaning.

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

    Available fields are:

    Field
    Description

    name

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

    label

    The label of your app.

    category

    Component behaviour files can be written in TypeScript. The engine runs TypeScript components using Node.js native type stripping, so no build step is required — upload the .ts files the same way as .js files. A component's main behaviour file can be either MyComponent.js or MyComponent.ts.

    Keep the following in mind:

    • When importing your own files (anything other than node_modules packages), the full file extension must be used: import { util } from './utils.ts'. See the Node.js TypeScript documentation for the supported subset of TypeScript.

    • Add "type": "module" to the connector's package.json to avoid module-type detection warnings and the associated performance overhead.

    • Only erasable TypeScript syntax is supported (type annotations, interfaces). Features that require transformation, such as enum or namespaces, are not.

    Introduction

    Directory Structure

    Services, modules and components hierarchy
    Google modules
    Twilio service
    {
        "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...."
    }    
    import { util } from './utils.ts';
    
    export default {
    
        async receive(context: any): Promise<any> {
            const message = context.messages.in.content;
            return context.sendJson({ result: util.process(message) }, 'out');
        }
    };

    Service Manifest File

    Module Manifest File

    TypeScript Components

    application/json

    Authorization

    Bearer <token>

    Body

    Name
    Type
    Description

    useCache

    boolean

    Default true.

    flow

    boolean

    Response

    {
        "components": {
    

    curl --location '[TENANT_API_URL]/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 } }'

    Get flow variables

    Content-Type

    Example:

    Toggle a custom loading state.

    Type: String | Default: null

    Toggle a custom error message.

    Submit the form and start the flow.

    Flow validation errors changed.

    Click a button to close the form.

    Submit the form and wait for the flow to start.

    const wizard = appmixer.ui.Wizard(config)
    
    wizard.set(key, value)
    wizard.get(key)
    wizard.state(name, value)

    Configuration

    config.el ...

    Learn about widgetconfig here.

    config.flowId

    Instance

    Learn about widget instance here.

    State

    loader

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

    error

    Events

    flow:start

    flow:validation

    cancel

    close

    Example

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

    A Step-by-step Guide

    Select a trigger

    Add Actions

    Create new variables and transform data with Modifiers

    Finalize our Workflow Automation

    Align Your Diagram

    Test Your Automation

    Start and Monitor Your Automation

    Making Changes to an Active Automation

    Select a trigger in your Automation
    Authenticate and configure your trigger

    Modifiers

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

    Get Modifiers

    GET [TENANT_API_URL]/modifiers

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

    {
        "categories": {
            "object": {
                "label": "Object",
                "index": 1
            },
            "list": {
                "label": "List",
                "index": 2
            },
            ...
    
    

    Edit Modifiers

    PUT [TENANT_API_URL]/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.

    Name
    Type
    Description

    DELETE [TENANT_API_URL]/modifiers

    Delete all modifiers. Restricted to admin users only.

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

    Name
    Type
    Description

    Unprocessed Messages

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

    Get messages

    GET [TENANT_API_URL]/unprocessed-messages

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

    Path Parameters

    Name
    Type
    Description

    GET [TENANT_API_URL]/unprocessed-messages/:messageId

    Get a single message.

    Name
    Type
    Description

    DELETE [TENANT_API_URL]/unprocessed-messages/:messageId

    Delete a message.

    Name
    Type
    Description

    POST [TENANT_API_URL]/unprocessed-messages/:messageId

    Put the message back into Appmixer engine.

    Name
    Type
    Description

    Insights Logs

    Browse logs of messages that passed through flows.

    Insights Logs

    Configuration

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

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

    config.el ...

    Learn about widget config .

    config.flowId

    ID of a flow to filter the logs by.

    Instance

    Learn about widget instance .

    State

    insightsLogs.state(name, value)

    loader

    Type: Boolean | Default: null

    Toggle a custom loading state.

    error

    Type: String | Default: null

    Toggle a custom error message. &#xNAN;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 the logs table. Example: Retrieving the current query from the widget's state.

    Set an initial query in the widget's state:

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

    Listen for changes triggered by UI interactions:

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

    Build and Run an AI agent

    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!

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

    ‍

    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

    Files

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

    GET [TENANT_API_URL]/files/metadata/:fileId

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

    Name
    Type
    Description

    Quotas & Limits

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

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

    An example of a quota module:

    The quota definition above tells the engine to throttle the

    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.

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

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

    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:

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

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

    UI & Widgets

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

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

    Type: String|Element | Default: null

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

    Type: Object | Default: DefaultTheme

    Custom theme definition.

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

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

    components

    object

    components.IDs

    array<string>

    Array of component IDs

    components.links

    boolean

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

    components.properties

    boolean

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

    string

    messageId

    string

    messageId

    string

    messageId

    string

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

    Get message

    Path Parameters

    Path Parameters

    Retry a message

    Path Parameters

    localization

    An optional object containing localization strings. For example:

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

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

    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

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

    helperFn

    string

    The Javascript helper function as a string.

    arguments

    array

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

    Request Body

    Delete Modifiers

    Test Modifier Function

    Request Body

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

    The arguments to be passed to the helper function.

    .

    ‍

    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.

    ‍

    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.

    ‍

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

    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.

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

    👉 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: https://docs.google.com/spreadsheets/d/1zWsRnL4-Sj-Rh-j3wlu9XdoIrj17uPWC7nMvL3vfskA/edit?usp=sharing

    ‍

    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: https://docs.google.com/spreadsheets/d/1JmyOaQxho-mBNS8iLm0uwL-X30G5tO4EK3Ha0MsLu7Q/edit?usp=sharing

    ‍

    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.

    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:

      • Scraping websites

      • Generating marketing content

      • Analyzing market trends

      • Writing product specifications

    What This AI Agent Does

    Step 1: Setting Up the AI Agent

    Choosing a Trigger

    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.

    Adding the AI Agent

    Handling the Agent’s Responses

    Step 2: Adding Skills to the AI Agent

    Connecting to External Tools

    Step 3: Testing and Deploying the AI Agent

    Expanding Your AI Agent’s Capabilities

    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"
    

    POST [TENANT_API_URL]/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.

    Name
    Type
    Description

    Content-type

    string

    Must be set to multipart/form-data

    uploader-file-id

    string

    Name
    Type
    Description

    file

    string

    The file/chunk to be uploaded

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

    GET [TENANT_API_URL]/files

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

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

    Name
    Type
    Description

    limit

    number

    offset

    number

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

    GET [TENANT_API_URL]/files/count

    Used for paging.

    Name
    Type
    Description

    includeComponentSourceFiles

    Boolean

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

    filter

    String

    {
        count: 2
    }

    DELETE [TENANT_API_URL]/files/:fileId

    {
        // Response
    }

    DELETE [TENANT_API_URL]/files

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

    Name
    Type
    Description

    filter

    String

    Can be used to filter files.

    {
        // Response
    }

    Get file info

    Path Parameters

    fileId

    Upload a file

    Headers

    Request Body

    Get user files

    Query Parameters

    Get number of files

    Query Parameters

    Remove file.

    Delete all user files.

    Query Parameters

    receive()
    call of the component to a max of 2000-times per day and 3-times per second.

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

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

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

    The time window in milliseconds.

    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:

    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.

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

    Quota Module Structure

    rules

    limit

    window

    throttling

    resource

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

    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:

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

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

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

    var appmixer = new Appmixer({ baseUrl: BASE_URL });
    appmixer.set('strings', STRINGS);
    appmixer.set('strings', {
        ui: {
            flowManager: {
                search: 'Search flows',
                header: {
                    buttonCreateFlow: 'Create new Flow'
                }
            }
        }
    });
    wget https://my.appmixer.com/appmixer/package/strings-en.json

    Setting Custom Strings

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

    An override applies when the key is present, whatever its value. A key set to an empty string, 0 or false overrides the shipped default rather than falling back to it; null and undefined render as an empty string, and other types are converted to strings. Only a key that is absent falls back to the default. The same rule applies to strings set through appmixer.set('strings', …), the instance l10n option and a widget's config.l10n.

    Before 6.5.1, overrides were resolved by truthiness, so a key deliberately set to an empty string, 0 or false was treated as absent and the shipped English default was rendered instead.

    Structure of the Strings Object

    Complete Strings Object

    Time Localization

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

    Pluralization and Strings with Variables

    Type: Object | Default: DefaultL10N

    Custom localization texts. See Custom Strings for the structure of the object and how overrides are resolved.

    Type: String | Default: en

    Language code for localization of components.

    Type: Object | Default: DefaultAPI

    Custom API methods.

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

    Unmount the widget instance and hide the el container.

    Reload the entire widget.

    Reset the state of the widget to defaults.

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

    Set config property.

    Get config property.

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

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

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

    Configuration

    config.el

    config.theme

    config.l10n

    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)

    config.lang

    config.api

    Instance

    widget.open

    widget.close

    widget.reload

    widget.reset

    widget.state

    Example

    widget.set

    widget.get

    widget.on

    widget.off

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

    categoryIndex

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

    index

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

    description

    Description of your app.

    icon

    App icon in the Data URI format.

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

    categoryIndex

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

    index

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

    description

    Description of your app.

    icon

    App icon in the Data URI format.

    Enabling Users to Publish Custom Components
    Enabling Users to Publish Custom Components
    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()

    Basic Usage

    Reading and Updating the Query with Nested Filters

    Tracking Query Changes

    Working with Nested Query Keys

    Example

    here
    here
    documentation

    inPorts

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

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

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

    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.

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

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

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

    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.

    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.

    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:

    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.

    Automation Hub

    Manage automation flows, logs, and connected accounts in a unified hub.

    Configuration

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

    const automationHub = appmixer.ui.AutomationHub(config)
    
    automationHub.set(key, value)
    automationHub.get(key)

    config.el ...

    Learn about widget config .

    config.options

    Type: Object | Default: {}

    config.options.customization.entryPoints.templates

    Type: Boolean | Default: true

    Show the option to start an automation from a pre-built template.

    config.options.customization.entryPoints.scratch

    Type: Boolean | Default: false

    Show the option to start an automation from a blank canvas.

    Type: Boolean | Default: true

    Toggle visibility of the widget header.

    Type: String[] | Default: []

    Tab keys to hide. Valid values: "flows", "logs", "accounts".

    Type: String | Default: "M"

    Icon size for flow instance tiles. Valid values: "S", "M", "L".

    Type: String | Default: "M"

    Icon size for template tiles. Valid values: "S", "M", "L".

    When a flow or template has an image field set (see the image property of the flow object in the ), the Automation Hub tile renders the image in the card body instead of the connector icon strip. Tiles of flows without an image keep showing the icon strip.

    Type: Object | Default: see below

    Override initial widget state. See State below for supported keys.

    Type: String | Default: "flows"

    The active tab. Valid values: "flows", "logs", "accounts".

    Type: String | Default: "grid"

    Flow list display mode. Valid values: "grid", "list".

    Type: String | Default: ""

    Text filter applied to the flows list.

    Type: Boolean | Default: false

    When true, only running flow instances are shown.

    Click to open a flow in the Designer. data.flow.flowId and data.flow.type identify the flow. data.isNew is true for newly created flows. There is no built-in handler — open an appmixer.ui.Designer instance in your handler.

    Click to open a flow in the Wizard. data.flow.flowId and data.flow.type identify the flow. There is no built-in handler — open an appmixer.ui.Wizard instance in your handler.

    Click to create a custom integration from a template. data.createFrom.sourceFlowId and data.createFrom.sourceFlowType identify the source template. Call next() to execute the default behavior (clone the template and open it in the Designer).

    Click to start a stopped flow. data.flow.flowId and data.flow.type identify the flow. Call next() to execute the default start behavior.

    Click to stop a running flow. data.flow.flowId and data.flow.type identify the flow. Call next() to execute the default stop behavior.

    Click to clone a flow. data.flow.flowId and data.flow.type identify the flow. Call next() to execute the default clone behavior.

    Click to rename a flow. data.flow.flowId and data.flow.type identify the flow. Call next() to open the built-in rename dialog.

    Click to delete a flow. data.flow.flowId and data.flow.type identify the flow. Call next() to open the built-in delete confirmation dialog.

    Click to view logs for a flow. data.flow.flowId and data.flow.type identify the flow. Call next() to navigate to the Logs tab for that flow.

    Audit Logs

    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.

    Accessing Audit Logs

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

    Using the Audit Logs Interface

    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.

    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

    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

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

    • Configuration Updated

    • Configuration Deleted

    • ACL Rule Created/Updated

    • Component Access Assigned

    • Flow Created

    • Flow Updated

    • Flow Started/Stopped

    • Flow Deleted

    • Integration Template Created

    • Integration Template Updated

    • Integration Template Published

    • Published Integration Template Updated

    • Integration Template Test Started

    • Integration Template Test Stopped

    • Integration Template Test Deleted

    • Connector Installed

    • Connector Updated

    • Connector Uninstalled

    • User Created

    • User Updated

    • User Deleted

    • User Logged In

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

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

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

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

    Integration Categories

    API for managing integration template categories

    Categories allow you to organize integration templates in the Automation Hub marketplace. Each template can be assigned to multiple categories, which can then be used to create custom tabs and filters in the marketplace widget.

    • Categories are global/system-wide resources

    • Admin-only access for create/update/delete operations

    • All authenticated users can read categories

    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.

    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.


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

    Integrations

    Manage flows used as integration templates and instances.

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

    Type: Object | Default: {}

    Type: Object | Default: {}

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

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

    uploader-file-name

    string

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

    uploader-chunk-size

    string

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

    uploader-chunk-number

    string

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

    uploader-chunks-total

    string

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

    sort

    String

    projection

    String

    includeComponentSourceFiles

    Boolean

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

    filter

    String

    here
    Flow Cloned
    Integration Template Deleted
    Login Failed
  • Password Changed

  • Applying Filters

    Viewing Audit Log Details

    Logged Events

    System Configuration

    Access Control

    Flow Management

    Integration Template Management

    Integration Template Testing

    Connector Management

    User Management

    Use Cases

    Security Compliance

    Troubleshooting

    User Activity Monitoring

    Change Tracking

    config.options.header.visible

    config.options.header.tabs.hidden

    config.options.flows.instances.tile.icons.size

    config.options.flows.templates.tile.icons.size

    Flow images on tiles

    config.state

    Instance

    Learn about widget instance here.

    State

    tab

    flows/layout

    flows/query/searchPattern

    flows/query/instances/onlyRunning

    Events

    Event handlers receive an object with event.data (event-specific fields) and event.next(). Calling event.next() executes the widget's built-in default behavior. If a handler is registered for an event, the default behavior is suppressed unless event.next() is called explicitly.

    flow:open-designer

    flow:open-wizard

    flow:create-custom

    flow:start

    flow:stop

    flow:clone

    flow:rename

    flow:remove

    flow:insights-logs

    Example

    Flows API
    here
    appmixer.ui.AutomationHub({
      /* ... */
      options: {
        header: {
          tabs: {
            hidden: ['logs', 'accounts']
          }
        }
      }
    })
    appmixer.ui.AutomationHub({
      /* ... */
      state: {
        tab: 'logs'
      }
    })
    automationHub.state(name, value)
    automationHub.on(event, handler)
    automationHub.on('flow:open-designer', ({ data }) => {/* ... */})
    automationHub.on('flow:open-wizard', ({ data }) => {/* ... */})
    automationHub.on('flow:create-custom', ({ data, next }) => {/* ... */})
    automationHub.on('flow:start', ({ data, next }) => {/* ... */})
    automationHub.on('flow:stop', ({ data, next }) => {/* ... */})
    automationHub.on('flow:clone', ({ data, next }) => {/* ... */})
    automationHub.on('flow:rename', ({ data, next }) => {/* ... */})
    automationHub.on('flow:remove', ({ data, next }) => {/* ... */})
    automationHub.on('flow:insights-logs', ({ data, next }) => {/* ... */})
    const designer = appmixer.ui.Designer({ el: '#designer' })
    
    const automationHub = appmixer.ui.AutomationHub({
        el: '#automation-hub',
        options: {
            customization: {
                entryPoints: {
                    templates: true,
                    scratch: true
                }
            }
        }
    })
    
    automationHub.on('flow:open-designer', ({ data }) => {
        designer.set('flowId', data.flow.flowId)
        designer.open()
    })
    
    automationHub.on('flow:open-wizard', ({ data }) => {
        const wizard = appmixer.ui.Wizard({
            el: '#wizard',
            flowId: data.flow.flowId
        })
        wizard.open()
    })
    
    automationHub.open()
    {
      "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 } }

    Templates can belong to multiple categories

  • Only integration templates and drafts can have categories

  • Deleting a category removes it from all associated templates

  • GET http://[API-URL]/categories

    Get all categories. Available to all authenticated users.

    [
      {
        "id"
    

    GET http://[API-URL]/categories/:categoryId

    Get details of a specific category. Available to all authenticated users.

    {
      "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
      "name": "CRM",
      "description": "Customer relationship management integrations",
      "created": 1678901234567,
      "mtime": 1678901234567
    }
    null

    POST http://[API-URL]/categories

    Create a new category. Admin token required.

    Name
    Type
    Description

    name*

    string

    Category name (1-100 characters), must be unique

    description

    string

    {
      "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
      "name": "CRM",
      "description": "Customer relationship management integrations",
      "created": 1678901234567,
      "mtime": 1678901234567
    }
    {
      "statusCode": 400,
      "error": "Bad Request",
      "message": "Category with this name already exists."
    }

    PUT http://[API-URL]/categories/:categoryId

    Update an existing category. Admin token required.

    Name
    Type
    Description

    name

    string

    Updated category name (1-100 characters)

    description

    string

    {
      "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
      "name": "CRM & Sales",
      "description": "Customer relationship management and sales tools",
      "created": 1678901234567,
      "mtime": 1678905678901
    }
    {
      "statusCode": 400,
      "error": "Bad Request",
      "message": "Category with this name already exists."
    }
    {
      "statusCode": 404,
      "error": "Not Found",
      "message": "Category not found."
    }

    DELETE http://[API-URL]/categories/:categoryId

    Delete a category. Admin token required. This will automatically remove the category from all templates that reference it.

    {
      "success": true
    }
    {
      "statusCode": 404,
      "error": "Not Found",
      "message": "Category not found."
    }

    Categories are assigned to integration templates through the Flow API. When creating or updating a flow, you can specify categories in the categories array.

    POST http://[API-URL]/flows

    PUT http://[API-URL]/flows/:flowId

    GET http://[API-URL]/flows/:flowId

    {
      "id": "796d7b5c-bea0-4594-a9df-a8a0e3c4616e",
      "name": "Salesforce to HubSpot Sync",
      "type": "integration-template",
      "categories": ["5f8a7b2c3d4e5f6a7b8c9d0e", "5f8a7b2c3d4e5f6a7b8c9d0f"],
      "flow": { ... },
      ...
    }

    When assigning categories to a template, the API validates:

    1. Category Existence: All category IDs must reference existing categories

    2. Flow Type: The flow must be an integration template or draft

    3. Array Format: Categories must be provided as an array of category IDs

    {
      "statusCode": 400,
      "error": "Bad Request",
      "message": "Category with ID invalid-category-id does not exist."
    }

    Categories are primarily used to organize templates in the Automation Hub marketplace:

    1. Create categories for your use cases (CRM, Marketing, Finance, etc.)

    2. Assign templates to relevant categories

    3. Configure marketplace tabs in Hub Settings to display templates by category

    4. End users browse templates organized by category in the marketplace widget

    All category operations are automatically logged in the audit trail:

    • Create: "Create category: [name]"

    • Update: "Update category: [name]"

    • Delete: "Delete category: [name]"

    Each log entry includes:

    • Operation type (create/update/delete)

    • Before and after state (for updates)

    • Timestamp and performing user

    See the Audit Logs documentation for more information.

    Overview

    curl -XGET "http://[API-URL]/categories" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    curl -XGET "http://[API-URL]/categories/5f8a7b2c3d4e5f6a7b8c9d0e" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    curl -XPOST "http://[API-URL]/categories" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "name": "CRM", "description": "Customer relationship management integrations" }'
    curl -XPUT "http://[API-URL]/categories/5f8a7b2c3d4e5f6a7b8c9d0e" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "name": "CRM & Sales", "description": "Customer relationship management and sales tools" }'
    curl -XDELETE "http://[API-URL]/categories/5f8a7b2c3d4e5f6a7b8c9d0e" \
      -H "Authorization: Bearer [ADMIN_TOKEN]"
    curl -XPOST "http://[API-URL]/flows" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "name": "Salesforce to HubSpot Sync",
        "type": "integration-template",
        "categories": ["5f8a7b2c3d4e5f6a7b8c9d0e", "5f8a7b2c3d4e5f6a7b8c9d0f"],
        "flow": { ... }
      }'
    curl -XPUT "http://[API-URL]/flows/[FLOW_ID]" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "categories": ["5f8a7b2c3d4e5f6a7b8c9d0e"]
      }'
    curl -XGET "http://[API-URL]/flows/[FLOW_ID]" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    # Invalid - category doesn't exist
    curl -XPOST "http://[API-URL]/flows" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "name": "My Template",
        "type": "integration-template",
        "categories": ["invalid-category-id"]
      }'
    # 1. Create categories
    curl -XPOST "http://[API-URL]/categories" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "name": "CRM", "description": "CRM integrations" }'
    
    # Response: { "id": "cat-crm-123", ... }
    
    curl -XPOST "http://[API-URL]/categories" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "name": "Marketing", "description": "Marketing tools" }'
    
    # Response: { "id": "cat-marketing-456", ... }
    
    # 2. Assign template to categories
    curl -XPUT "http://[API-URL]/flows/my-template-id" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "categories": ["cat-crm-123", "cat-marketing-456"] }'
    
    # 3. End users see the template in both CRM and Marketing tabs

    List Categories

    Get Category

    Create Category

    Request Body

    Unique Names

    Category names must be unique across the system. If you attempt to create a category with a name that already exists, the API will return a 400 Bad Request error.

    Update Category

    Request Body

    Delete Category

    Automatic Cleanup

    When a category is deleted, it is automatically removed from all integration templates and drafts that reference it. This ensures data consistency without requiring manual cleanup.

    Assigning Categories to Templates

    Create Flow with Categories

    Update Flow Categories

    Get Flow with Categories

    Category Restrictions

    Flow Type Restrictions

    Categories can only be assigned to:

    • Integration templates (integration-template)

    Category Validation

    Validation Example

    Use Cases

    Marketplace Organization

    Example Workflow

    Audit Logging

    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.


    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.

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

    • Repository: appmixer-connectors/src/appmixer/mcpservers

    • Example: GitLab MCP Server Connector

    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.

    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. &#xNAN;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 &#xNAN;(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.


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


    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.

    Component

    Purpose

    Understanding MCP Server Integration in Appmixer

    MCP Server Connector Components

    Where it’s used

    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

    How to Implement a Custom MCP Server Connector

    Step 1 – Start from an Existing Example

    Step 2 – Files to Modify

    Packaging and Publishing

    Summary Checklist

    Filter the integrations with additional parameters:

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

    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:

    Type: Boolean | Default: null

    Toggle a custom loading state.

    Type: String | Default: null

    Toggle a custom error message.

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

    Click a button to edit integration.

    Click a button to remove integration.

    Click a button to start integration.

    Click a button to stop integration.

    const integrations = appmixer.ui.Integrations(config)
    
    integrations.set(key, value)
    integrations.get(key)

    Configuration

    config.el ...

    Learn about widget config here.

    config.options

    config.options.customFilter

    Integrations
    appmixer.ui.Integrations({
      /* ... */
      options: {
        customFilter: {
          // displaying Integrations with a certain customField
          'customFields.category': 'healthcare',
          // that are shared with someone
          'sharedWith': '![]'
          // and in this example, that are shared throught the 'domain'
          // options with a 'testdomain.com'
          'sharedWith.domain': 'testdomain.com'
          // or we could filter only Integrations that are shared with
          // scope 'user'
          // 'sharedWith.scope': 'user',
        }
      }
    }
    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()

    Updating the Custom Filter Dynamically

    Instance

    Learn about widget instance .

    State

    loader

    error

    Events

    integration:create

    integration:edit

    integration:remove

    integration:start

    integration:stop

    Example

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

    inPort.schema

    inPort.inspector

    inPort.source

    Optional query parameters for the source call

    ignoreAuth=true

    silentAuth=true

    inPort.variablesPipeline

    inPort.maxConnections

    Properties Schema
    Properties Inspector

    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 Appmixer Github repository , 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.

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

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

    Now, you need to initialize the CLI tool to configure it for your Appmixer tenant and log in with an admin account. Before you publish your components, verify that your admin account is To do this, visit your Appmixer tenant's backoffice at [TENANT_BACKOFFICE_URL]. 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 [TENANT_API_URL] and [TENANT_BACKOFFICE_URL] refer to the URLs of your Appmixer tenant provided to you during the signup process, for example https://api-acme.appmixer.ai and https://backoffice-acme.appmixer.ai (newer hosted tenants), or https://api.acme.appmixer.cloud and https://backoffice.acme.appmixer.cloud (older hosted tenants).

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

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

    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 .

    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 for our input while the inspector.inputs section to collect input from the user.

    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.

    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:

    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.

    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.

  • Integration drafts (integration-draft)

    Attempting to assign categories to other flow types (e.g., flow, flow-template) will result in a 400 Bad Request error.

    :
    "
    5f8a7b2c3d4e5f6a7b8c9d0e
    "
    ,
    "name": "CRM",
    "description": "Customer relationship management integrations",
    "created": 1678901234567,
    "mtime": 1678901234567
    },
    {
    "id": "5f8a7b2c3d4e5f6a7b8c9d0f",
    "name": "Marketing",
    "description": "Marketing automation and analytics tools",
    "created": 1678901234568,
    "mtime": 1678901234568
    }
    ]

    Category description (max 500 characters)

    Updated description (max 500 characters)

    $ npm install -g appmixer
    $ appmixer url [TENANT_API_URL]
    $ appmixer login your@admin.com
    Password:
    $ appmixer init component appmixer.boredapi.core.GetActivity \
    --description "Get a random activity to do when I am bored." \
    --author "Appmixer Team <info@appmixer.com>" \
    --inPorts in \
    --outPorts out \
    --iconUri "https://cdn.iconscout.com/icon/free/png-256/free-bored-267462.png" \
    --serviceLabel BoredAPI \
    --serviceDescription "Get random activities."
    $ tree appmixer
    appmixer
    └── boredapi
        ├── core
        │   └── GetActivity
        │       ├── GetActivity.js
        │       ├── component.json
        │       └── package.json
        └── service.json
    
    3 directories, 4 files
    {
        "name": "appmixer.boredapi.core.GetActivity",
        "description": "Get a random activity to do when I am bored.",
        "icon": "data:image/png;base64,iVBORw0KGgoAA...",
        "author": "Appmixer Team <info@appmixer.com>",
        "inPorts": [{
            "name": "in",
            "schema": {
                "type": "object",
                "properties": {}
            },
            "inspector": {
                "inputs": {}
            }
        }],
        "outPorts": [{
            "name": "out",
            "options": []
        }]
    }
    {
        "name": "appmixer.boredapi.core.GetActivity",
        "description": "Get a random activity to do when I am bored.",
        "icon": "data:image/png;base64,iVBORw0KGgoAA...",
        "author": "Appmixer Team <info@appmixer.com>",
        "inPorts": [{
            "name": "in",
            "schema": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "education", "recreational", "cooking"
                        ]
                    }
                }
            },
            "inspector": {
                "inputs": {
                    "type": {
                        "type": "select",
                        "index": 0,
                        "label": "Type",
                        "tooltip": "Type of the activity",
                        "options": [
                            { "content": "Education", "value": "education" },
                            { "content": "Recreational", "value": "recreational" },
                            { "content": "Cooking", "value": "cooking" }
                        ]
                    }
                }
            }
        }],
        "outPorts": [{
            "name": "out",
            "options": [
                { "label": "Activity", "value": "activity" },
                { "label": "Accessibility", "value": "accessibility" },
                { "label": "Type", "value": "type" },
                { "label": "Participants", "value": "participants" },
                { "label": "Price", "value": "price" }
            ]
        }]
    }
    module.exports = {
        receive: async function(context) {
            let url = '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

    Install Appmixer CLI

    Create your Component

    Define Input and Output

    Call 3rd Party HTTP API

    Test your Component

    Publish your Component

    Appmixer CLI
    associated with a "vendor."
    Bored API endpoint
    JSON Schema
    defines the form
    Type input
    Output of GetActivity consumed in the connected SendEmail component
    here

    Embed into Your Application

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

    Install the Appmixer SDK

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

    <script src="[TENANT_STUDIO_URL]/appmixer/package/appmixer.js"></script>

    Replace [TENANT_STUDIO_URL] with the URL of your Appmixer Studio, for example https://acme.appmixer.ai (newer hosted tenants) or https://my.acme.appmixer.cloud (older hosted tenants).

    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. Replace [TENANT_API_URL] below with it, for example https://api-acme.appmixer.ai (newer hosted tenants) or https://api.acme.appmixer.cloud (older hosted tenants).

    const appmixer = new Appmixer({ baseUrl: '[TENANT_API_URL]' });

    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.

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

    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 .

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

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

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

    The Automation Hub is an all-in-one widget that combines a marketplace for your automations, wizard for end-users interacting with the automations, logs and accounts.

    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:

    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.

    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:

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

    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 page in the documentation.

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

    User

    API for users

    Sign-in User

    POST [TENANT_API_URL]/user/auth

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

    Request Body

    Name
    Type
    Description

    POST [TENANT_API_URL]/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": "abc@example.com", "email": "abc@example.com", "password": "abc321" }'

    Name
    Type
    Description

    GET [TENANT_API_URL]/user

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

    GET [TENANT_API_URL]/users/:userId

    Admin token required.

    GET [TENANT_API_URL]/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]'

    GET [TENANT_API_URL]/users/count

    Admin token required

    PUT [TENANT_API_URL]/users/:userId

    Admin token required.

    Name
    Type
    Description

    DELETE [TENANT_API_URL]/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 [TENANT_API_URL]/users/:userId/delete-status/:ticket

    POST [TENANT_API_URL]/user/change-password

    User token required.

    Name
    Type
    Description

    POST [TENANT_API_URL]/user/reset-password

    Admin token required.

    Name
    Type
    Description

    POST [TENANT_API_URL]/forgot-password

    See the configuration for more details.

    Name
    Type
    Description

    POST [TENANT_API_URL]/forgot-password/reset

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

    Name
    Type
    Description

    Loops

    Repeat a part of a flow for every item of a list: the Each component with Sequential turned on and Continue Each at the end of the loop.

    A loop runs the same steps for every item of a list: every contact from a search, every row of a spreadsheet, every file in a folder. In Appmixer a loop is built from components of the Controls utility module.

    The recommended way to build a loop is a :

    • Each with Sequential turned on takes a list and sends one item at a time.

    • The steps connected to the item port of Each are the loop body. They run once for every item.

    outPorts

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

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

    An example of an outPorts definition can look like this:

    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

    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

    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

    username

    string

    Email address.

    metadata

    object

    Optional metadata.

    aiContext

    string

    Optional per-user context for . Admin token required.

    metadata

    Object

    Optional metadata.

    email

    String

    Email address.

    aiContext

    String

    Optional per-user context for . Send null to clear it.

    password*

    string

    Password.

    username*

    string

    Username, has to have an email format.

    password*

    string

    Password.

    email

    string

    {
        "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI",
        "user": {
            "id": "5c88c7cc04a917256c726c3d",
            "username": "abc@example.com",
            "isActive": true,
            "email": "abc@example.com",
            "scope": [
                "user"
            ],
            "metadata": {},
            "aiContext": null,
            "plan": "beta"
        }
    }
    {
      "id": "58593f07c3ee4f239dc69ff7",
      "username": "tomas@client.io",
      "isActive": true,
      "email": "tomas@client.io",
      "scope": [
        "user"
      ],
      "metadata": {},
      "aiContext": "Timezone: Europe/Prague.",
      "plan": "beta"
    }

    scope

    Array

    Array of scopes.

    vendor

    String|Array

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

    oldPassword*

    String

    Old password

    newPassword*

    String

    email*

    String

    User email address

    password*

    String

    email*

    String

    Email address

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

    password*

    String

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

    code*

    String

    {}

    Create User

    Request Body

    aiContext always requires an admin token, even though this endpoint is otherwise open by default. Copilot splices the value into its system prompt, so allowing a self-service signup to set it would be a prompt-injection vector. A request that sets aiContext without the admin scope is rejected with 403 Setting aiContext requires the admin scope. Reading the field back is unrestricted.

    Get Current User Information

    Get User Information

    Get all users

    Get the number of users

    Update user

    Request Body

    Delete user

    Change user password

    Request Body

    Reset user password

    Request Body

    Forgot Password

    Request Body

    Reset forgotten password

    Request Body

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

    Email address.

    One or more vendors.

    New password

    New password

    Code generated via forgot-password.

    Continue Each at the end of the loop body confirms that the item is done, so Each sends the next one. It can also add values from the item to the Result of the loop.

  • Once all items are done, Each sends done with the collected Result, one entry per item, in the order of the list.

  • The items are processed one at a time and in their order, every step in the loop body can use the variables of the components before Each, and you get the results of the whole loop without Storage or Join Each.

    A sequential loop: Continue Each closes the loop body

    When the order does not matter and speed does, Each can also run the items differently:

    • Parallel loop with Join Each (Sequential off, the default of the component): all items are sent at once and processed in parallel, so they can finish in any order. Join Each collects them.

    • Delayed loop: the items are sent one after another with a pause between them, for example to stay under an API rate limit.

    Set Field to a variable that holds a list, for example the result of a Find or List component. The list can also be a string with a JSON array. Every message on the item port contains:

    • Index: the position of the item in the list, starting at 0.

    • Value: the item itself. When the list has a known schema, the Designer also offers the item's properties as separate variables.

    • Items Count: the number of items in the list.

    • Correlation ID: the ID of this run of the loop (see below).

    Once all items are done, Each sends one message on the done port with Items Count, Correlation ID and Result. An empty list sends no item, only done with a count of 0.

    A flow can run the same loop several times at once. A trigger may fire twice in a second, or an Each may be nested inside another Each. All these runs send their items through the same components, so the messages of different runs are mixed together on the way. The Correlation ID is what keeps them apart.

    Each creates one Correlation ID per run. Every time a list arrives on the in port, Each generates a new Correlation ID and puts it in every item message and in the done message of that run. Two runs never share it. The ID is stable, though: if the engine delivers the same input message again after an error, the loop resumes with the same Correlation ID.

    Continue Each finds the waiting run by the Correlation ID. In a sequential loop, map the Correlation ID of the Each item to Continue Each. It tells Continue Each which run of which loop to continue, see The Correlation ID in a sequential loop.

    Nested loops. When a loop body contains another Each, every run of the inner Each gets its own Correlation ID. Pair every Continue Each (or Join Each) with the Each on the same level: the inner one uses the Correlation ID of the inner Each, the outer one the Correlation ID of the outer Each.

    With Sequential off, Each sends all items at once. To wait until the loop body has processed all of them and get them as one list, add Join Each. It has two input ports:

    • in: connect the last step of the loop body. Every message that arrives here is one processed item.

    • count: connect the done port of Each and map its Items Count to Number of input messages. This tells Join Each how many messages to wait for.

    When the given number of messages has arrived, Join Each sends them all as Array of items on its out port.

    A parallel loop with Each and Join Each
    Join Each: both ports use the Correlation ID of the same Each

    If some items never arrive (a step failed, or a Condition in the loop body filtered them out), Join Each would wait forever. That is why Timeout is on by default: after Timeout (seconds) (300 by default) Join Each sends what has arrived to its timeout port, together with the expected count and the number of items that actually arrived.

    Join Each groups messages by the Correlation ID. It keeps a separate counter for every Correlation ID it sees. A message on in is added to the group of its Correlation ID, and the count from done is set for the group with the same Correlation ID. A group is complete when its number of messages reaches its own count. It does not matter which comes first: the done message usually arrives before the loop body has finished, but it can also arrive after it. Both ports of Join Each must therefore get the Correlation ID of the same Each:

    • On the in port, set Correlation ID to the Correlation ID variable of the Each item. The variable is available on the last step of the loop body, because every message in the loop carries the variables of the item it started from.

    • On the count port, set Correlation ID to the Correlation ID variable of the Each done port.

    Mapping the Correlation ID of an outer Each into the Join Each of an inner loop would merge the results of all the inner loops into one group that never reaches its count.

    The demo flow loops over the list [1500, 100, 800] in parallel. The Slow step (a Code Block) takes as many milliseconds as the item says, and Join Each collects the results. The items finish, and arrive at Join Each, in the order 100, 800, 1500:

    Create the flow with the POST /flows endpoint, or with the Appmixer CLI (appmixer flow create demo-each-join.json).

    Each (Sequential) ──item──▶ step ▶ step ▶ Continue Each
          │
          └──done──▶ steps after the loop (use the collected Result)

    Introduction

    sequential loop
    Each ──item──▶ Step A ──▶ Step B ──▶ Join Each (in)     Correlation ID = Each › item › Correlation ID
      └───done───────────────────────────▶ Join Each (count)  Correlation ID = Each › done › Correlation ID
                                                              Number of input messages = Each › done › Items Count
    {
        "items": [
            { "result": "item 100 ms" },
            { "result": "item 800 ms" },
            { "result": "item 1500 ms" }
        ]
    }

    Other options (advanced)

    The Each component

    Without Sequential, done means "all items have been sent", not "all items have been processed". The loop body may still be working on them, and Result is empty. Use a when the next step has to wait for the whole loop.

    How the Correlation ID connects the loop

    Parallel loop with Join Each

    This is an advanced option. For most loops, use a : it keeps the order and collects the results without Join Each.

    Join Each collects the items in the order in which they arrive, not in the order of the list. If the order matters, sort the result by the item's Index, or use a .

    Demo flow

    demo-each-join.json
    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 .

    Every variable in the Designer's variables picker can show a sample value so that the user building a flow can see what the data behind a variable actually looks like before the flow has ever run. You provide those samples with the example keyword in the output port schema.

    Add example next to type and title on each property:

    If your output port uses options rather than a top-level schema, put example inside the per-option schema object instead:

    Nested objects and arrays. Give the object or array property itself a structured example that mirrors its properties / items:

    A nested variable takes its sample from the example of its parent, keyed by the property name. One structured example on the parent therefore covers the parent variable and all of its children at once, while an example placed only on a deeply nested leaf is not displayed.

    A sample for the whole message. You can also define a single example object on the output port schema itself. Its keys are used for any top-level property that does not carry its own example, which is convenient when you want to paste one real API response into the manifest:

    Which sample wins. Appmixer resolves the value shown in the picker in this order, first match wins:

    1. Real data recorded from previous runs of the flow, when it is available.

    2. default

    3. example

    4. The first non-empty entry of the JSON Schema examples array.

    example is the keyword to use in a manifest. The examples array is only understood so that schemas imported from OpenAPI specifications keep working, and default should be reserved for its actual meaning — a default value — rather than added just to drive the preview.

    Guidelines

    • Copy realistic values from a real API response: a real ID format, a real timestamp, a real URL. Placeholders such as "string" or "value" are worse than no example at all.

    • Match the declared type. A boolean example is false, not "false"; an integer example is 0, not "0".

    • Falsy samples (0, false, "") are rendered, so do not omit them out of concern that they will not show.

    • Use title for the label the user sees in the picker. description is not rendered there — help text belongs on the inspector instead.

    • Never paste real customer data, tokens or personal e-mail addresses into an example. Use obviously fictional values and example.com addresses.

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

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

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

    Dynamic output port options.

    Here is an example of the UpdatedRow output port definition.

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

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

    JSON Schema

    Variables Picker
    {
        "outPorts": [
            {
                "name": "weather",
                "options": [
                    { "label": "Temperature", "value": "main.temp" },
                    { "label": "Pressure", "value": "main.pressure" },
                    { "label": "Humidity", "value": "main.humidity" },
                    { "label": "Sunrise time (unix, UTC)", "value": "sys.sunrise" },
                    { "label": "Sunset time (unix, UTC)", "value": "sys.sunset" },
                    { "label": "City name", "value": "name" },
                    { 
                        "label": "Weather data", 
                        "value": "weather", 
                        "schema": {
                            "type": "array",
                            "items": {
                                "type": "object",
                                "properties": {
                                    "description": { "type": "string", "title": "Weather description" },
                                    "icon": { "type": "string", "title": "Weather icon code" },
                                    "iconUrl": { "type": "string", "title": "Weather icon URL" }
                                }    
                            }
                        }
                    }
                ]
            }
        ]
    }
    {
        "outPorts": [
            {
                "name": "weather",
                "schema": {
                    "type": "object",
                    "properties": {
                        { "title": "Temperature", "value": "main.temp" },
                        { "title": "Pressure", "value": "main.pressure" },
                        { "title": "Humidity", "value": "main.humidity" },
                        { "title": "Sunrise time (unix, UTC)", "value": "sys.sunrise" },
                        { "title": "Sunset time (unix, UTC)", "value": "sys.sunset" },
                        { "title": "City name", "value": "name" },
                        { 
                            "label": "Weather data", 
                            "value": "weather", 
                            "schema": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "description": { "type": "string", "title": "Weather description" },
                                        "icon": { "type": "string", "title": "Weather icon code" },
                                        "iconUrl": { "type": "string", "title": "Weather icon URL" }
                                    }
                                }                            
                            }
                        }
                    }
                }
            }
        ]
    }
    {
        "outPorts": [
            {
                "name": "out",
                "schema": {
                    "type": "object",
                    "properties": {
                        "id": { "type": "string", "title": "Task ID", "example": "86cbb4hct" },
                        "name": { "type": "string", "title": "Name", "example": "Ship the release notes" },
                        "url": { "type": "string", "title": "URL", "example": "https://app.clickup.com/t/86cbb4hct" },
                        "date_created": { "type": "string", "title": "Date Created", "example": "1787901544394" },
                        "time_estimate": { "type": "number", "title": "Time Estimate (ms)", "example": 3600000 },
                        "archived": { "type": "boolean", "title": "Archived", "example": false }
                    }
                }
            }
        ]
    }
    {
        "outPorts": [
            {
                "name": "out",
                "options": [
                    { "label": "Email Message ID", "value": "id", "schema": { "type": "string", "example": "18d4b5c3e7f2a1b0" } },
                    { "label": "Snippet", "value": "snippet", "schema": { "type": "string", "example": "Your order #12345 has been shipped..." } },
                    { "label": "SizeEstimate", "value": "sizeEstimate", "schema": { "type": "integer", "example": 3200 } }
                ]
            }
        ]
    }
    {
        "assignees": {
            "type": "array",
            "title": "Assignees",
            "example": [
                { "id": 62612899, "username": "Jane Doe", "email": "jane.doe@example.com" }
            ],
            "items": {
                "type": "object",
                "properties": {
                    "id": { "type": "number", "title": "ID" },
                    "username": { "type": "string", "title": "Username" },
                    "email": { "type": "string", "title": "Email" }
                }
            }
        }
    }
    {
        "outPorts": [
            {
                "name": "out",
                "schema": {
                    "type": "object",
                    "example": {
                        "id": "86cbb4hct",
                        "name": "Ship the release notes"
                    },
                    "properties": {
                        "id": { "type": "string", "title": "Task ID" },
                        "name": { "type": "string", "title": "Name" }
                    }
                }
            }
        ]
    }
    {
        "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"
                        }
                    }
                }
            }
        ]
    }
    

    Example values

    Example values are also part of the fallback chain that produces sample output when a flow is executed in test mode. See .

    outPort.source

    outPort.maxConnections

    JSON Schema
    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:

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

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

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

    It follows the same pattern as in components, but we use the service/module path as a key for the definition:

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

    Use the localization strings to do so:

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

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

    2. Strings object components namespace.

    3. Property in the manifest (component.json).

    For example, when resolving an input's label, it will first look if there is a localization object in the manifest with a path to that input's label. If not, it will search the Strings Object. If none of that is defined, it will use values from the manifest.

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

    Component's manifest localization object

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

    Strings object's component namespace

    service.json and module.json localization

    Example using localization object in service.json

    Example using Strings object

    Application groups localization

    Strings resolving

    JSON path
    function generateSecureUsertoken(length = 22) {
      const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
      return Array.from(crypto.getRandomValues(new Uint32Array(length)))
        .map((x) => charset[x % charset.length])
        .join('');
    }
    
    generateSecureUsertoken()  // ODQMwnwGeZQeXTV5sj3AsR
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Appmixer Automation Hub – Minimal Demo</title>
    </head>
    <body>
      <div id="widget"></div>
    
      <script src="[TENANT_STUDIO_URL]/appmixer/package/appmixer.js"></script>
    
      <script type="module">
        const API_BASE_URL = '[TENANT_API_URL]';
        const USERNAME = 'YOUR_USERNAME';
        const PASSWORD = 'YOUR_PASSWORD';
    
        const appmixer = new Appmixer({ baseUrl: API_BASE_URL, debug: true });
        const { token } = await appmixer.api.authenticateUser(USERNAME, PASSWORD);
        appmixer.set('accessToken', token);
    
        appmixer.ui.AutomationHub({
        el: '#widget',
        state: {
            flows: {
                layout: 'grid'
            }
        },
        options: {
            customization: {
                entryPoints: {
                    templates: true,
                    scratch: false
                }
            },
            header: {
                visible: true,
                tabs: {
                    hidden: []
                },
                subheader: {
                    visible: false
                }
            },
            flows: {
                header: {
                    layout: {
                        visible: true
                    }
                },
                templates: {
                    header: {
                        categories: {
                            visible: false,
                            tabs: []
                        }
                    }
                }
            }
        },
        l10n: {
            ui: {
                automationHub: {}
            }
        },
        theme: {
            mode: 'dark',
            variables: {
                colors: {
                    surface: '#2A2A2A',
                    neutral: '#FFFFFF',
                    primary: '#2B75EF',
                    onPrimary: '#FFFFFF',
                    secondary: '#94A6D4',
                    onSecondary: '#FFFFFF',
                    tertiary: '#D494D0',
                    onTetriary: '#FFFFFF',
                    error: '#EF4444',
                    warning: '#F6C20C',
                    onWarning: '#FFFFFF',
                    success: '#01C58D',
                    onSuccess: '#FFFFFF',
                    modifier: '#C558CF',
                    onModifier: '#FFFFFF',
                    highlighter: '#FFA500',
                    separator: '#4C4C4C',
                    charcoalTeal: '#2C3130',
                    darkJade: '#2C4B42'
                },
                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,
                    size: 14
                },
                shadows: {
                    level0: 'none',
                    level1: 'none',
                    level2: 'none',
                    level3: 'none',
                    level4: 'none',
                    level5: 'none',
                    backdrop: 'rgba(0 0 0 / 92%)',
                    popover: '1px 3px 9px rgba(0 0 0 / 32%)',
                    icon: 'none',
                    blur: 'rgba(0 0 0 / 75%)',
                    bar: 'none'
                }
            }
        }
    }).open();
      </script>
    </body>
    </html>
    <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'
            }
        }
    });
    

    Embed the Automation Hub

    Embed Integration Marketplace (legacy widget)

    Embed Automation Designer

    Custom Strings and Localization

    Custom Theme

    Demo Applications

    Custom Strings
    Custom Theme
    https://github.com/Appmixer-ai/appmixer-customer-support-demo-app
    https://github.com/Appmixer-ai/appmixer-demo-embedded-integrations
    https://github.com/Appmixer-ai/appmixer-demo-embedded-designer
    https://github.com/Appmixer-ai/appmixer-demo-firebase-vanilla
    Appmixer Virtual Users
    Default - not yet themed - embedded Integration Marketplace
    Appmixer Designer Embedded in your SaaS product
    Appmixer Flow Manager Embedded in your SaaS product
    Changed UI Strings in embedded Flow Manager

    Data Stores

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

    Get All Stores

    GET [TENANT_API_URL]/stores

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

    [{
        "name": "
    

    Get One Store metadata

    GET [TENANT_API_URL]/stores/:id

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

    Name
    Type
    Description

    GET [TENANT_API_URL]/store/count

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

    Name
    Type
    Description

    GET [TENANT_API_URL]/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]"

    Name
    Type
    Description

    Both and accept the same two search parameters, pattern and escapePattern.

    pattern is a regular expression that is matched against both the key and the value of every record. A record is included in the result if either of them matches. The pattern is not anchored, so it matches anywhere in the key or the value.

    The two fields are matched slightly differently:

    • the key is matched case-sensitively,

    • the value is matched case-insensitively and . also matches newline characters (useful for multi-line JSON values).

    If pattern is not provided, all the records of the store are returned (or counted).

    Remember to URL-encode the pattern, especially when it contains characters such as +, &, ? or #.

    Set escapePattern to true to search for the pattern literally instead of interpreting it as a regular expression. All the regular expression special characters (- [ ] { } ( ) * + ? . , \ ^ $ | # and whitespace) in pattern are escaped before the search is performed.

    Use it whenever the pattern comes from a user input (a search box, for example). Without it, a user typing ( or * would either get unexpected results or an invalid regular expression.

    escapePattern is a string parameter and escaping is applied only when its value is exactly true. Any other value (including 1, yes or True) leaves the pattern to be interpreted as a regular expression. The default is not to escape.

    Without escapePattern=true, the same pattern would be interpreted as a regular expression with a capturing group and would match records containing price USD instead.

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

    Name
    Type
    Description

    DELETE [TENANT_API_URL]/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]"

    Name
    Type
    Description

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

    Name
    Type
    Description
    Name
    Type
    Description

    POST [TENANT_API_URL]/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"

    Name
    Type
    Description
    Name
    Type
    Description

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

    Name
    Type
    Description
    Name
    Type
    Description

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

    Name
    Type
    Description

    GET [TENANT_API_URL]/store/download/:storeId

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

    Name
    Type
    Description

    User Groups

    API for user groups (workspaces)

    User Groups (also known as Workspaces) enable teams to collaborate by sharing resources such as flows, data stores, and files. All resources are owned by the group rather than individual users, while maintaining audit trails for individual user actions.

    Overview

    • All group resources are shared among group members

    • Individual user actions are tracked in audit logs

    • Admin users are automatically added to the "admin" user group

    • Users can switch between personal and group contexts

    • JWT tokens include group information for proper authorization

    POST [TENANT_API_URL]/user-groups

    Create a new user group. Admin token required.

    Name
    Type
    Description

    GET [TENANT_API_URL]/user-groups

    List all user groups. Admin users see all groups, regular users see only their own groups.

    GET [TENANT_API_URL]/user-groups/:groupId

    Get details of a specific user group. Users can only access groups they belong to, admins can access all groups.

    PUT [TENANT_API_URL]/user-groups/:groupId

    Update user group details. Admin token required.

    Name
    Type
    Description

    DELETE [TENANT_API_URL]/user-groups/:groupId

    Delete a user group. Admin token required. This will remove the group and revoke all group member access to shared resources.

    Deleting a group that still owns flows is rejected with a 400 error — transfer the group's resources first (see ), or pass ?force=true to delete the group together with all its resources.

    Name
    Type
    Description

    PUT [TENANT_API_URL]/resources/transfer

    Transfer all resources (flows, accounts, access tokens, flow versions, data stores, files, telemetry, ...) from a user or group to another user or group. Admin token required.

    Typical use cases: an employee leaving (transfer their resources to a group or a colleague), or consolidating group ownership.

    The transfer runs as a background task — the endpoint returns 202 Accepted with a ticket. The source user is blocked (metadata.blocked) before the transfer starts to prevent API access during the migration; requesting a transfer for an already-blocked source returns 409 Conflict. Running flows keep running during and after the transfer.

    Provide exactly one of sourceUserId/sourceGroupId and exactly one of targetUserId/targetGroupId.

    Name
    Type
    Description

    POST [TENANT_API_URL]/user-groups/:groupId/members

    Add one or more members to a user group. Admin token required.

    Name
    Type
    Description

    DELETE [TENANT_API_URL]/user-groups/:groupId/members/:userId

    Remove a member from a user group. Admin token required. This will revoke all issued tokens for the removed user in this group context.

    GET [TENANT_API_URL]/user-groups/:groupId/members

    Get all members of a user group. Users can only access groups they belong to, admins can access all groups.

    GET [TENANT_API_URL]/users/:userId/groups

    Get all groups that a specific user belongs to.

    POST [TENANT_API_URL]/auth/switch-context

    Switch user context between personal workspace and a group workspace. Returns a new JWT token with the selected context.

    Name
    Type
    Description

    GET [TENANT_API_URL]/auth/available-contexts

    List all available contexts (personal and groups) that the current user can switch to.

    User groups are ideal for teams that need to collaborate on integrations and automations:

    1. Create a group for your team (e.g., "Marketing Team")

    2. Add team members to the group

    3. Members switch context to the group workspace

    4. All resources (flows, accounts, data stores) are now shared among team members

    When a user is assigned the admin scope, they are automatically added to the default "Admin Group". This enables:

    • Shared access to all integration templates

    • Collaborative administration of the Appmixer instance

    • Shared ownership of admin-created resources

    User groups work with SSO (Single Sign-On) authentication:

    • JWT tokens from SSO providers should include a groups claim

    • Users are automatically assigned to groups based on the SSO groups claim

    • Tokens are refreshed correctly to maintain group membership

    Sequential Loop

    Process the items of a list strictly one at a time, in their original order, with the Sequential mode of Each and the Continue Each component.

    Introduction

    In the default mode the Each component sends all items at once. When the steps in the loop body take a different amount of time for different items, the fast items overtake the slow ones. A delay only spaces the items out; it does not wait for them to finish.

    A sequential loop is the recommended way to build a loop in Appmixer. Turn on Sequential on Each and end the loop body with the Continue Each component. Each then sends the next item only after the previous one has been confirmed by Continue Each, so the items are processed one at a time and in their order, and the loop collects a Result from every item.

    A sequential loop: Continue Each closes the loop body

    How it works

    1. Each with Sequential turned on sends the first item.

    2. The steps in the loop body process the item.

    3. Continue Each confirms the item and adds its entry to the result.

    4. Each sends the next item. After the last item has been confirmed, Each sends done with the collected Result.

    Every item continues the flow run that started the loop, so the steps in the loop body can use the variables of the components before Each for every item, not only the first one.

    • Sequential: send the next item only after Continue Each has confirmed the previous one.

    • Item Timeout (seconds): how long Each waits for the confirmation of one item. Default 300, minimum 60. An item that is not confirmed in time is skipped, and the loop moves on to the next one, so it never hangs. This happens when a step in the loop body fails, or when a Condition filters the item out before it reaches Continue Each. Set the timeout safely above the longest time one item can take.

    • Delay: in a sequential loop, an extra pause in milliseconds after an item is confirmed and before the next one is sent.

    Place Continue Each after the last step that has to finish before the next item starts.

    • Correlation ID: the Correlation ID variable of the Each item. It tells Continue Each which loop to continue (see below).

    • Index: the Index variable of the Each item. It tells Each which item is done. A repeated or late confirmation, for example for an item that has already timed out, is ignored.

    • Add to Result (optional): rows of Name and Value, the values to collect from the item, for example id

    Continue Each passes Correlation ID and Index on its out port, so the flow can continue after it if needed.

    The Correlation ID works as described in : Each creates one per run and puts it in every item. In a sequential loop it also identifies the run that is waiting. While a run is waiting for its item to be confirmed, Each keeps the rest of the list under its Correlation ID. Continue Each looks up the waiting run by that ID and wakes it up.

    • Always map the Correlation ID of the item of the Each you want to continue. If Continue Each gets an ID that no sequential loop is waiting for (Sequential is off, or the loop has already finished), it only logs it and passes the message on.

    • In nested sequential loops, every Each needs its own Continue Each with its own Correlation ID and Index. The inner Continue Each goes at the end of the inner loop body, the outer one after everything the outer item has to finish, including the inner loop (for example, on the done port of the inner Each).

    Every row of Add to Result in Continue Each has a Name and a Value. Each item adds one entry to the Result list that Each sends on its done port once all items are processed: an object with one field per row. The entries are in item order and line up with the items, so you do not need Join Each or Storage in a sequential loop.

    For example, with the rows id → ID of the created record and status → Status of the created record:

    • A value that is empty for an item is added as null in its field, so every entry has the same fields.

    • An item that was skipped after Item Timeout is null as a whole (the second entry above).

    • Without rows in Add to Result, every entry is an empty object {}.

    The done port also contains Items Count and Correlation ID.

    The demo flow loops over the list [1500, 100, 800]. The Slow step (a Code Block) takes as many milliseconds as the item says. Without Sequential the items would finish in the order 100, 800, 1500. With Sequential they are processed in the order 1500, 100, 800, and the done port returns:

    The notes in the flow explain each part on the canvas. Create the flow with the endpoint, or with the Appmixer CLI:

    API Module

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

    Name
    Description

    api.set(name, value)

    Set configuration property.

    api.get(name)

    Get configuration property.

    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.

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

    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.

    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.

    Delete an existing flow identified by flowId.

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

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

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

    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.

    Start a flow.

    Stop a flow.

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

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

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

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

    Get the number of records in a store. query is an object with the following properties:

    • storeId - ID of the store.

    • pattern - optional regular expression matched against the keys and the values of the records. Only records whose key or value matches it are counted. If not provided, all the records of the store are counted.

    • escapePattern - optional, set to true to search for

    See in the Data Stores API reference for the details of pattern and escapePattern.

    Get store records. query is an object with the following properties:

    • storeId - ID of the store.

    • pattern - optional regular expression matched against the keys and the values of the records. Only records whose key or value matches it are returned. If not provided, all the records of the store are returned.

    • escapePattern - optional, set to true to search for

    Example:

    See in the Data Stores API reference for the details of pattern and escapePattern.

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

    Delete a store.

    Rename an existing store.

    Create a new record in a store.

    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.

    Create a custom account.

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

    Get a list of accounts connected to a specific component.

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

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

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

    Get logs of a specific flow:

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

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

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

    Return one task identified by id.

    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.

    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.

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

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

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

    Handle Flow Errors

    Error handling can be configured per component, directly in the flow. Open the component's context menu in the Designer (cogwheel → Error handling) or set the errorHandling object on the component descriptor via the API (see ):

    • Automatic retry — whether the engine retries the failed message, with an optional override of the number of retries.

    • On error — what happens once retries are exhausted:

    Insights

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

    GET [TENANT_API_URL]/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]"

    For efficient pagination through large result sets, use searchAfter instead of from

    AI Copilot
    AI Copilot

    targetUserId

    string

    User to transfer resources to.

    targetGroupId

    string

    Group to transfer resources to.

    Audit logs maintain individual user accountability

    name*

    string

    Name of the user group

    metadata

    object

    name

    string

    Updated name of the group

    metadata

    object

    force

    boolean

    Delete the group even if it still owns resources; all its resources are removed too.

    sourceUserId

    string

    User to transfer resources from.

    sourceGroupId

    string

    userIds*

    string[]

    Array of user IDs to add

    groupId

    string

    Group ID to switch to, or null/omit to switch to personal context

    Create User Group

    Request Body

    List User Groups

    Get User Group

    Update User Group

    Request Body

    Delete User Group

    Query Parameters

    Transfer Resources

    Request Body

    Add Group Members

    Request Body

    Remove Group Member

    Get Group Members

    List User's Groups

    Switch Context

    Request Body

    JWT Token Structure with Groups

    When operating in a group context, the JWT token includes:

    • id: The group user ID (resources are owned by this ID)

    • originalUserId: The actual user ID (for audit logging)

    • groups: Array of group IDs the user belongs to

    This ensures proper resource ownership while maintaining audit trails.

    Get Available Contexts

    Use Cases

    Collaborative Teams

    Admin Group

    SSO Integration

    Important Notes

    Token Revocation

    When a user is removed from a group, all issued JWT tokens for that user in the group context are automatically revoked. However, if a token is being used inside a running flow component, it will continue to work until the component execution completes.

    Audit Trail

    All actions performed by users in a group context are logged with the original user ID, ensuring full audit trail capability even when resources are shared.

    Resource Ownership

    When operating in a group context:

    • All created resources (flows, accounts, data stores, files) are owned by the group user ID

    • All group members have equal access to these resources

    • Switching back to personal context shows only personally-owned resources

    Transfer Resources

    Optional metadata for the group

    Updated metadata (replaces existing)

    Group to transfer resources from.

    pattern
    literally instead of interpreting it as a regular expression. Use it when the pattern comes from a user input.
    pattern
    literally instead of interpreting it as a regular expression. Use it when the pattern comes from a user input.
  • limit - maximum number of records returned. Defaults to 1000.

  • offset - index of the first record returned. Defaults to 0.

  • sort - record property to sort by, followed by : and the sort order -1 (descending) or 1 (ascending).

  • api.on(event, handler)

    Add event listener.

    api.off(event, handler)

    Remove event listener.

    Methods

    api.authenticateUser

    api.authenticateWithEmailAndPassword

    api.signupUser

    api.createFlow

    api.deleteFlow

    api.getFlow

    api.getFlows

    api.getFlowsCount

    api.updateFlow

    api.startFlow

    api.stopFlow

    api.cloneFlow

    api.getUser

    api.getStores

    api.getStore

    api.getStoreRecordsCount

    api.getStoreRecords

    api.createStore

    api.deleteStore

    api.renameStore

    api.createStoreItem

    api.deleteStoreItems

    api.createAccount

    api.getAccounts

    api.getComponentAccounts

    api.getAccountFlows

    api.setAccountName

    api.getLogs

    api.getLog

    api.getPeopleTasks

    api.getPeopleTasksCount

    api.getPeopleTask

    api.approveTask

    api.rejectTask

    api.getFlowAuthentication

    Events

    error

    warning

    Data URI
    Searching Records
    Searching Records
    GET /accounts
    sequential loop
    sequential loop
    sequential loop
    → the ID of a record created in the loop. See
    .

    A row with a value but no name stops Continue Each with an error.

  • Result is an empty list when Sequential is off.

  • Settings

    Each

    Continue Each

    The Correlation ID in a sequential loop

    Collecting results

    Demo flow

    demo-each-sequential.json
    How the Correlation ID connects the loop
    POST /flows
    Each with Sequential turned on
    Continue Each: Add to Result with a Name and a Value
    Collecting results
    curl -XPOST "https://api.appmixer.com/user-groups" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "name": "Marketing Team", "metadata": { "department": "marketing" } }'
    {
      "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
      "name": "Marketing Team",
      "userId": "group-1678901234567",
      "metadata": {
        "department": "marketing"
      },
      "created": 1678901234567,
      "members": []
    }
    curl -XGET "https://api.appmixer.com/user-groups" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    [
      {
        "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
        "name": "Marketing Team",
        "userId": "group-1678901234567",
        "metadata": {
          "department": "marketing"
        },
        "created": 1678901234567,
        "members": ["user1", "user2"]
      },
      {
        "id": "5f8a7b2c3d4e5f6a7b8c9d0f",
        "name": "Admin Group",
        "userId": "group-1678901234568",
        "metadata": {},
        "created": 1678901234568,
        "members": ["admin1"]
      }
    ]
    curl -XGET "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    {
      "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
      "name": "Marketing Team",
      "userId": "group-1678901234567",
      "metadata": {
        "department": "marketing"
      },
      "created": 1678901234567,
      "members": ["user1", "user2", "user3"]
    }
    curl -XPUT "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "name": "Marketing & Sales Team", "metadata": { "department": "marketing", "region": "EMEA" } }'
    {
      "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
      "name": "Marketing & Sales Team",
      "userId": "group-1678901234567",
      "metadata": {
        "department": "marketing",
        "region": "EMEA"
      },
      "created": 1678901234567
    }
    curl -XDELETE "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e" \
      -H "Authorization: Bearer [ADMIN_TOKEN]"
    {
      "success": true
    }
    curl -XPUT "https://api.appmixer.com/resources/transfer" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "sourceUserId": "5f8a7b2c3d4e5f6a7b8c9d0e", "targetGroupId": "64b0a1c2d3e4f5a6b7c8d9e0" }'
    {
      "ticket": "2e9dd726-2b7f-46f7-bea4-8db7f7175aa8"
    }
    {
      "error": "A transfer for this source is already in progress or has been completed"
    }
    curl -XPOST "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e/members" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "userIds": ["user1", "user2", "user3"] }'
    {
      "added": ["user1", "user2", "user3"],
      "group": {
        "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
        "name": "Marketing Team",
        "members": ["user1", "user2", "user3"]
      }
    }
    curl -XDELETE "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e/members/user1" \
      -H "Authorization: Bearer [ADMIN_TOKEN]"
    {
      "success": true,
      "removedUserId": "user1",
      "revokedTokens": 3
    }
    curl -XGET "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e/members" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    {
      "members": [
        {
          "id": "user1",
          "username": "john@example.com",
          "email": "john@example.com"
        },
        {
          "id": "user2",
          "username": "jane@example.com",
          "email": "jane@example.com"
        }
      ]
    }
    curl -XGET "https://api.appmixer.com/users/user1/groups" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    {
      "groups": [
        {
          "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
          "name": "Marketing Team",
          "userId": "group-1678901234567",
          "metadata": {
            "department": "marketing"
          }
        },
        {
          "id": "5f8a7b2c3d4e5f6a7b8c9d0f",
          "name": "Admin Group",
          "userId": "group-1678901234568",
          "metadata": {}
        }
      ]
    }
    curl -XPOST "https://api.appmixer.com/auth/switch-context" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "groupId": "5f8a7b2c3d4e5f6a7b8c9d0e" }'
    {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "context": {
        "type": "group",
        "groupId": "5f8a7b2c3d4e5f6a7b8c9d0e",
        "groupName": "Marketing Team",
        "originalUserId": "user1"
      },
      "user": {
        "id": "group-1678901234567",
        "username": "group-1678901234567",
        "scope": ["user"],
        "type": "group"
      }
    }
    curl -XGET "https://api.appmixer.com/auth/available-contexts" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    {
      "personal": {
        "type": "personal",
        "userId": "user1",
        "username": "john@example.com"
      },
      "groups": [
        {
          "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
          "name": "Marketing Team",
          "userId": "group-1678901234567",
          "type": "group"
        },
        {
          "id": "5f8a7b2c3d4e5f6a7b8c9d0f",
          "name": "Admin Group",
          "userId": "group-1678901234568",
          "type": "group"
        }
      ],
      "current": {
        "type": "personal",
        "userId": "user1"
      }
    }
    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.getStoreRecordsCount({
        storeId: '5c6d643f4849f447eba55c1d',
        pattern: 'price (USD)',
        escapePattern: true
    });
    await appmixer.api.getStoreRecords(query)
    await appmixer.api.getStoreRecords({
        limit: 30,
        offset: 0,
        pattern: 'price (USD)',
        escapePattern: true,
        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.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 => { ... }
    {
        "name": "Demo - Each + Join Each",
        "flow": {
            "e2b0c002-0000-4000-8000-000000000001": {
                "type": "appmixer.utils.controls.OnStart",
                "x": 64,
                "y": 64,
                "version": "1.0.0",
                "source": {},
                "config": {},
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e2b0c002-0000-4000-8000-000000000002": {
                "type": "appmixer.utils.controls.SetVariable",
                "label": "Items",
                "x": 272,
                "y": 64,
                "version": "1.0.0",
                "source": {
                    "in": {
                        "e2b0c002-0000-4000-8000-000000000001": [
                            "out"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e2b0c002-0000-4000-8000-000000000001": {
                                "out": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "variables": {}
                                    },
                                    "lambda": {
                                        "variables": {
                                            "ADD": [
                                                {
                                                    "type": "text",
                                                    "name": "durations",
                                                    "text": "[1500, 100, 800]"
                                                }
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e2b0c002-0000-4000-8000-000000000003": {
                "type": "appmixer.utils.controls.Each",
                "x": 480,
                "y": 64,
                "version": "1.5.0",
                "source": {
                    "in": {
                        "e2b0c002-0000-4000-8000-000000000002": [
                            "out"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e2b0c002-0000-4000-8000-000000000002": {
                                "out": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "list": {
                                            "b1000000-0000-4000-8000-000000000001": {
                                                "variable": "$.e2b0c002-0000-4000-8000-000000000002.out.durations",
                                                "functions": []
                                            }
                                        }
                                    },
                                    "lambda": {
                                        "list": "{{{b1000000-0000-4000-8000-000000000001}}}"
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e2b0c002-0000-4000-8000-000000000004": {
                "type": "appmixer.utils.controls.CodeBlock",
                "label": "Slow step",
                "x": 688,
                "y": 64,
                "version": "1.0.0",
                "source": {
                    "in": {
                        "e2b0c002-0000-4000-8000-000000000003": [
                            "item"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e2b0c002-0000-4000-8000-000000000003": {
                                "item": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "code": {},
                                        "variables": {
                                            "b1000000-0000-4000-8000-000000000002": {
                                                "variable": "$.e2b0c002-0000-4000-8000-000000000003.item.value",
                                                "functions": []
                                            }
                                        }
                                    },
                                    "lambda": {
                                        "code": "(function() { var end = Date.now() + Number($data.ms); while (Date.now() < end) {} return 'item ' + $data.ms + ' ms'; })()",
                                        "variables": {
                                            "ADD": [
                                                {
                                                    "type": "text",
                                                    "name": "ms",
                                                    "text": "{{{b1000000-0000-4000-8000-000000000002}}}"
                                                }
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e2b0c002-0000-4000-8000-000000000005": {
                "type": "appmixer.utils.controls.JoinEach",
                "x": 928,
                "y": 224,
                "version": "1.1.2",
                "source": {
                    "in": {
                        "e2b0c002-0000-4000-8000-000000000004": [
                            "out"
                        ]
                    },
                    "count": {
                        "e2b0c002-0000-4000-8000-000000000003": [
                            "done"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e2b0c002-0000-4000-8000-000000000004": {
                                "out": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "correlationId": {
                                            "b1000000-0000-4000-8000-000000000003": {
                                                "variable": "$.e2b0c002-0000-4000-8000-000000000003.item.correlationId",
                                                "functions": []
                                            }
                                        }
                                    },
                                    "lambda": {
                                        "correlationId": "{{{b1000000-0000-4000-8000-000000000003}}}"
                                    }
                                }
                            }
                        },
                        "count": {
                            "e2b0c002-0000-4000-8000-000000000003": {
                                "done": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "count": {
                                            "b1000000-0000-4000-8000-000000000004": {
                                                "variable": "$.e2b0c002-0000-4000-8000-000000000003.done.count",
                                                "functions": []
                                            }
                                        },
                                        "correlationId": {
                                            "b1000000-0000-4000-8000-000000000005": {
                                                "variable": "$.e2b0c002-0000-4000-8000-000000000003.done.correlationId",
                                                "functions": []
                                            }
                                        },
                                        "timeout": {},
                                        "timeoutValue": {}
                                    },
                                    "lambda": {
                                        "count": "{{{b1000000-0000-4000-8000-000000000004}}}",
                                        "correlationId": "{{{b1000000-0000-4000-8000-000000000005}}}",
                                        "timeout": true,
                                        "timeoutValue": 300
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            }
        },
        "notes": {
            "n1": {
                "x": 48,
                "y": -144,
                "width": 560,
                "height": 136,
                "content": "## Each + Join Each\n\n**Each** sends every item of the list to **Slow step** at once. **Join Each** waits until all of them are processed and sends them as one list.\n\nBoth Join Each ports use the **Correlation ID** of the same Each, so parallel runs of this flow never mix."
            },
            "n2": {
                "x": 48,
                "y": 272,
                "width": 520,
                "height": 150,
                "content": "## Join Each mapping\n\n- **in** - Correlation ID: *Each › item › Correlation ID*\n- **count** - Number of input messages: *Each › done › Items Count*\n- **count** - Correlation ID: *Each › done › Correlation ID*\n\nThe items arrive in the order they finish: 100, 800, 1500."
            }
        }
    }
    [
        { "id": "1001", "status": "created" },
        null,
        { "id": "1003", "status": "created" }
    ]
    {
        "count": 3,
        "correlationId": "cid:2407358777",
        "result": [
            { "result": "item 1500 ms" },
            { "result": "item 100 ms" },
            { "result": "item 800 ms" }
        ]
    }
    appmixer flow create demo-each-sequential.json
    {
        "name": "Demo - Each sequential",
        "flow": {
            "e1a0c001-0000-4000-8000-000000000001": {
                "type": "appmixer.utils.controls.OnStart",
                "x": 64,
                "y": 64,
                "version": "1.0.0",
                "source": {},
                "config": {},
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e1a0c001-0000-4000-8000-000000000002": {
                "type": "appmixer.utils.controls.SetVariable",
                "label": "Items",
                "x": 272,
                "y": 64,
                "version": "1.0.0",
                "source": {
                    "in": {
                        "e1a0c001-0000-4000-8000-000000000001": [
                            "out"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e1a0c001-0000-4000-8000-000000000001": {
                                "out": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "variables": {}
                                    },
                                    "lambda": {
                                        "variables": {
                                            "ADD": [
                                                {
                                                    "type": "text",
                                                    "name": "durations",
                                                    "text": "[1500, 100, 800]"
                                                }
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e1a0c001-0000-4000-8000-000000000003": {
                "type": "appmixer.utils.controls.Each",
                "label": "Each (sequential)",
                "x": 480,
                "y": 64,
                "version": "1.5.0",
                "source": {
                    "in": {
                        "e1a0c001-0000-4000-8000-000000000002": [
                            "out"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e1a0c001-0000-4000-8000-000000000002": {
                                "out": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "list": {
                                            "a1000000-0000-4000-8000-000000000001": {
                                                "variable": "$.e1a0c001-0000-4000-8000-000000000002.out.durations",
                                                "functions": []
                                            }
                                        },
                                        "sequential": {},
                                        "itemTimeout": {}
                                    },
                                    "lambda": {
                                        "list": "{{{a1000000-0000-4000-8000-000000000001}}}",
                                        "sequential": true,
                                        "itemTimeout": 120
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e1a0c001-0000-4000-8000-000000000004": {
                "type": "appmixer.utils.controls.CodeBlock",
                "label": "Slow step",
                "x": 688,
                "y": 64,
                "version": "1.0.0",
                "source": {
                    "in": {
                        "e1a0c001-0000-4000-8000-000000000003": [
                            "item"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e1a0c001-0000-4000-8000-000000000003": {
                                "item": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "code": {},
                                        "variables": {
                                            "a1000000-0000-4000-8000-000000000003": {
                                                "variable": "$.e1a0c001-0000-4000-8000-000000000003.item.value",
                                                "functions": []
                                            }
                                        }
                                    },
                                    "lambda": {
                                        "code": "(function() { var end = Date.now() + Number($data.ms); while (Date.now() < end) {} return 'item ' + $data.ms + ' ms'; })()",
                                        "variables": {
                                            "ADD": [
                                                {
                                                    "type": "text",
                                                    "name": "ms",
                                                    "text": "{{{a1000000-0000-4000-8000-000000000003}}}"
                                                }
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e1a0c001-0000-4000-8000-000000000005": {
                "type": "appmixer.utils.controls.ContinueEach",
                "x": 896,
                "y": 64,
                "version": "1.0.0",
                "source": {
                    "in": {
                        "e1a0c001-0000-4000-8000-000000000004": [
                            "out"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e1a0c001-0000-4000-8000-000000000004": {
                                "out": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "correlationId": {
                                            "a1000000-0000-4000-8000-000000000004": {
                                                "variable": "$.e1a0c001-0000-4000-8000-000000000003.item.correlationId",
                                                "functions": []
                                            }
                                        },
                                        "index": {
                                            "a1000000-0000-4000-8000-000000000005": {
                                                "variable": "$.e1a0c001-0000-4000-8000-000000000003.item.index",
                                                "functions": []
                                            }
                                        },
                                        "result": {
                                            "a1000000-0000-4000-8000-000000000006": {
                                                "variable": "$.e1a0c001-0000-4000-8000-000000000004.out.result",
                                                "functions": []
                                            }
                                        }
                                    },
                                    "lambda": {
                                        "correlationId": "{{{a1000000-0000-4000-8000-000000000004}}}",
                                        "index": "{{{a1000000-0000-4000-8000-000000000005}}}",
                                        "result": {
                                            "ADD": [
                                                {
                                                    "name": "result",
                                                    "value": "{{{a1000000-0000-4000-8000-000000000006}}}"
                                                }
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            },
            "e1a0c001-0000-4000-8000-000000000006": {
                "type": "appmixer.utils.controls.SetVariable",
                "label": "All done",
                "x": 688,
                "y": 256,
                "version": "1.0.0",
                "source": {
                    "in": {
                        "e1a0c001-0000-4000-8000-000000000003": [
                            "done"
                        ]
                    }
                },
                "config": {
                    "transform": {
                        "in": {
                            "e1a0c001-0000-4000-8000-000000000003": {
                                "done": {
                                    "type": "json2new",
                                    "modifiers": {
                                        "variables": {
                                            "a1000000-0000-4000-8000-000000000007": {
                                                "variable": "$.e1a0c001-0000-4000-8000-000000000003.done.result",
                                                "functions": []
                                            },
                                            "a1000000-0000-4000-8000-000000000008": {
                                                "variable": "$.e1a0c001-0000-4000-8000-000000000003.done.count",
                                                "functions": []
                                            }
                                        }
                                    },
                                    "lambda": {
                                        "variables": {
                                            "ADD": [
                                                {
                                                    "type": "text",
                                                    "name": "results",
                                                    "text": "{{{a1000000-0000-4000-8000-000000000007}}}"
                                                },
                                                {
                                                    "type": "text",
                                                    "name": "count",
                                                    "text": "{{{a1000000-0000-4000-8000-000000000008}}}"
                                                }
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "errorHandling": {
                    "autoRetry": false,
                    "onError": "stopFlow"
                }
            }
        },
        "notes": {
            "n0000000-0000-4000-8000-000000000001": {
                "x": 48,
                "y": -176,
                "width": 560,
                "height": 168,
                "content": "## Each - Sequential mode demo\n\nThe list holds three items. **Slow step** takes as many milliseconds as the item says: **1500, 100, 800**.\n\nA plain Each sends all items at once, so the fast items overtake the slow one and finish as 100, 800, 1500.\n\nWith **Sequential** on, Each sends the next item only after **Continue Each** confirms the previous one, so the items are processed strictly as 1500, 100, 800."
            },
            "n0000000-0000-4000-8000-000000000002": {
                "x": 656,
                "y": -224,
                "width": 440,
                "height": 208,
                "content": "## The loop body\n\n**Slow step** stands in for any slow step (an API call, a database write...).\n\n**Continue Each** closes the loop. Map to it:\n- **Correlation ID** and **Index** from Each's item\n- **Add to Result**: `result` = the output of Slow step\n\nIf an item is not confirmed within **Item Timeout** (120 s here), Each skips it and moves on."
            },
            "n0000000-0000-4000-8000-000000000003": {
                "x": 864,
                "y": 224,
                "width": 400,
                "height": 120,
                "content": "## Done\n\nWhen every item is confirmed, Each fires **done**.\n\n**Result** has one entry per item, in item order:\n`[{\"result\": \"item 1500 ms\"}, {\"result\": \"item 100 ms\"}, {\"result\": \"item 800 ms\"}]`"
            }
        }
    }

    escapePattern

    string

    Optional. Set to true to treat pattern as a literal string instead of a regular expression. See .

    offset

    number

    Index of the first record returned. Defaults to 0.

    limit

    number

    Maximum number of records returned. Defaults to 1000.

    pattern

    string

    Optional search pattern. Only records whose key or value matches the pattern are returned. See .

    escapePattern

    string

    Optional. Set to true to treat pattern as a literal string instead of a regular expression. See .

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

    id

    string

    Store ID.

    storeId

    string

    Store ID. If omitted, records of all the stores of the current user are counted.

    pattern

    string

    {
        "count": 681
    }

    storeId

    string

    Store ID. If omitted, records of all the stores of the current user are returned.

    sort

    string

    [{
        "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"
    }]
    # All records whose key or value contains "shirt".
    curl "[TENANT_API_URL]/store?storeId=5b213e0ef90a6200113abfd4&pattern=shirt" \
        -H "Authorization: Bearer [ACCESS_TOKEN]"
    
    # All records whose key or value starts with "T-shirt".
    curl "[TENANT_API_URL]/store?storeId=5b213e0ef90a6200113abfd4&pattern=%5ET-shirt" \
        -H "Authorization: Bearer [ACCESS_TOKEN]"
    
    # All records whose key or value contains a 4-digit number.
    curl "[TENANT_API_URL]/store?storeId=5b213e0ef90a6200113abfd4&pattern=%5Cd%7B4%7D" \
        -H "Authorization: Bearer [ACCESS_TOKEN]"
    # Finds records containing the literal string "price (USD)".
    curl -G "[TENANT_API_URL]/store" \
        --data-urlencode "storeId=5b213e0ef90a6200113abfd4" \
        --data-urlencode "pattern=price (USD)" \
        --data-urlencode "escapePattern=true" \
        -H "Authorization: Bearer [ACCESS_TOKEN]"

    name

    string

    Name of the store.

    {
        "storeId": "5c7f9bfe51dbaf0007f08db0"
    }

    id

    string

    Store ID.

    id

    string

    Store ID.

    name

    string

    New name of the store.

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

    key

    string

    Key under which the posted value will be stored.

    id

    string

    string

    Value to store under the key.

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

    id*

    String

    Store ID

    key*

    String

    key

    String

    New key

    value

    String

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

    items

    array

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

    {
        "deletedCount": 1
    }

    format

    String

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

    {
        // Response
    }

    Path Parameters

    Get Number of Records in a Store

    Query Parameters

    Get Store Records

    Query Parameters

    Searching Records

    pattern

    escapePattern

    When you paginate through search results, pass the same pattern and escapePattern to both /store and /store/count so that the total count matches the records you list.

    Create a new Store

    Request Body

    Delete a Store

    Path Parameters

    Rename a Store

    Path Parameters

    Request Body

    Create a new Store Item

    Path Parameters

    Request Body

    Update key or value of an existing store item

    Path Parameters

    Request Body

    Delete Store Items

    Request Body

    Download the content of a Data Store

    Query Parameters

    Get Store Records
    Get Number of Records in a Store
    {
        "name": "My Store 1",
        "storeId": "5c6fc9932ff3ff000747ead4"
    }

    Optional search pattern. Only records whose key or value matches the pattern are counted. See .

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

    Store ID.

    Key under which the updates are required

    New Value

    Error port — the component gets a virtual error output port that you can wire to any downstream component (for example, a Slack notification or your own error-handling branch). The connected component receives the error details (error.message, error.code, error.name, input) together with the flow data of preceding components, available in the variable picker like any other output port.

  • Stop flow — the whole flow is stopped.

  • Stop execution (default) — the failed data message is stored in Unprocessed Messages. Other data messages processed succesfully within the flow are stored in the Logs as usual.

  • When the WEBHOOK_FLOW_COMPONENT_ERROR system webhook (described below) is configured and the error was routed to the error port, the webhook payload is labelled with sentToErrorPort: true.

    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.

    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:

    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.

    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:

    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.

    When Database Limits are enabled, Appmixer can notify you when users approach or exceed their storage quotas. These notifications are sent via system webhooks, allowing you to proactively manage capacity and communicate with users about their storage usage.

    Configure the following system webhook variables in the Appmixer Backoffice to receive storage quota notifications:

    Triggered when a user reaches the warning threshold (default: 90% of soft limit).

    Payload example:

    Triggered when a user exceeds their soft limit but remains below the hard limit.

    Payload example:

    Triggered when a user exceeds their hard limit. At this point, write operations are blocked, jobs are paused, and incoming webhooks are rejected.

    Payload example:

    System-wide capacity events notify administrators when the entire system approaches or reaches capacity limits.

    Triggered when system storage utilization reaches the warning threshold (default: 80%).

    Payload example:

    Triggered when system storage utilization reaches the critical threshold (default: 95%). At this point, the circuit breaker activates and blocks write operations system-wide.

    Payload example:

    Storage quota notifications respect a cooldown period (default: 24 hours) to prevent notification spam. Once a notification is sent for a specific user and event type, the same notification will not be sent again until the cooldown period expires, even if the user remains in the same quota state.

    Storage quota webhooks enable you to:

    • Send branded emails to users about their storage usage

    • Trigger automated cleanup processes

    • Alert administrators about users approaching limits

    • Create dashboards showing storage utilization trends

    • Implement tiered storage plans with automatic upgrades

    • Proactively manage system capacity before reaching critical levels

    For more information about configuring and managing database limits, see Database Limits.

    Per-Component Error Handling

    Component Error Handling
    {
        "err": {
            "message": "Validation error on ports: in",
            "code": "GRID_ERR_VAL_PORTS",
            "name": "ValidationFlowError",
            "stack": "ValidationFlowError: Validation error on ports: in\n    at MessagesProcessor.process (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/MessagesProcessor.js:67:19)\n    at Context.prepare (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/Context.js:68:31)\n    at ContextHandler.createContext (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/ContextHandler.js:60:17)\n    at async InputQueue.consume (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/InputQueue.js:151:23)\n    at async InputQueue.onMessage (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/AMQPClient.js:378:20)"
        },
        "type": "component",
        "flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
        "flowName": "New flow",
        "userId": "5f804b96ea48ec47a8c444a7",
        "componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
        "componentType": "appmixer.utils.email.SendEmail",
        "inputMessages": {
            "in": [
                {
                    "properties": {
                        "correlationId": "339bc448-a806-4e61-8d38-4211fcedaf12",
                        "contentType": "application/json",
                        "contentEncoding": "utf8",
                        "sender": {
                            "componentId": "e8c581b4-9985-4f2c-bf30-895bf1d5541b",
                            "type": "appmixer.utils.controls.OnStart",
                            "outputPort": "out"
                        },
                        "destination": {
                            "componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
                            "inputPort": "in"
                        },
                        "flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
                        "messageId": "47293e2c-4e33-4558-8805-386de392ef04",
                        "flowRunId": 1607683798995
                    },
                    "content": {
                        "to": "2020-12-11T10:49:59.050Z"
                    },
                    "scope": {
                        "e8c581b4-9985-4f2c-bf30-895bf1d5541b": {
                            "out": {
                                "started": "2020-12-11T10:49:59.050Z"
                            }
                        }
                    },
                    "originalContent": {
                        "started": "2020-12-11T10:49:59.050Z"
                    }
                }
            ]
        }
    }
    {
        "err": {
            "message": "Validation error on ports: in",
            "error": {
                "in": [
                    [
                        {
                            "keyword": "format",
                            "dataPath": ".to",
                            "schemaPath": "#/properties/to/format",
                            "params": {
                                "format": "email"
                            },
                            "message": "should match format \"email\""
                        }
                    ]
                ]
            },
            "code": "GRID_ERR_VAL_PORTS",
            "name": "ValidationFlowError",
            "stack": "ValidationFlowError: Validation error on ports: in\n    at MessagesProcessor.process (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/MessagesProcessor.js:67:19)\n    at Context.prepare (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/Context.js:68:31)\n    at ContextHandler.createContext (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/ContextHandler.js:60:17)\n    at async InputQueue.consume (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/InputQueue.js:151:23)\n    at async InputQueue.onMessage (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/AMQPClient.js:378:20)"
        },
        "type": "component",
        "flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
        "flowName": "New flow",
        "userId": "5f804b96ea48ec47a8c444a7",
        "componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
        "componentType": "appmixer.utils.email.SendEmail",
        "inputMessages": {
            "in": [
                {
                    "properties": {
                        "correlationId": "339bc448-a806-4e61-8d38-4211fcedaf12",
                        "contentType": "application/json",
                        "contentEncoding": "utf8",
                        "sender": {
                            "componentId": "e8c581b4-9985-4f2c-bf30-895bf1d5541b",
                            "type": "appmixer.utils.controls.OnStart",
                            "outputPort": "out"
                        },
                        "destination": {
                            "componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
                            "inputPort": "in"
                        },
                        "flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
                        "messageId": "47293e2c-4e33-4558-8805-386de392ef04",
                        "flowRunId": 1607683798995
                    },
                    "content": {
                        "to": "2020-12-11T10:49:59.050Z"
                    },
                    "scope": {
                        "e8c581b4-9985-4f2c-bf30-895bf1d5541b": {
                            "out": {
                                "started": "2020-12-11T10:49:59.050Z"
                            }
                        }
                    },
                    "originalContent": {
                        "started": "2020-12-11T10:49:59.050Z"
                    }
                }
            ]
        }
    }
    {
        "err": {
            "stack": "Error: getaddrinfo ENOTFOUND mandrillapp.com\n    at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:66:26)",
            "message": "getaddrinfo ENOTFOUND mandrillapp.com",
            "errno": "ENOTFOUND",
            "code": "ENOTFOUND",
            "syscall": "getaddrinfo",
            "hostname": "mandrillapp.com",
            "name": "Error"
        },
        "type": "component",
        "flowId": "0ab287ef-7bd6-4cc3-b53b-c916c857cbe7",
        "flowName": "Invalid email test",
        "userId": "5fd744d5e9ed7d0011ca35f9",
        "componentId": "cb3f4ff5-7b6e-4d24-b7a8-2115c8254baa",
        "componentType": "appmixer.utils.email.SendEmail",
        "inputMessages": {
            "in": [
                {
                    "properties": {
                        "correlationId": "254ad628-f9c1-4483-81ed-33a22ac3ddc6",
                        "contentType": "application/json",
                        "contentEncoding": "utf8",
                        "sender": {
                            "componentId": "bcbeda1d-9036-45af-a25d-a57cf06e3f90",
                            "type": "appmixer.utils.controls.OnStart",
                            "outputPort": "out"
                        },
                        "destination": {
                            "componentId": "cb3f4ff5-7b6e-4d24-b7a8-2115c8254baa",
                            "inputPort": "in"
                        },
                        "flowId": "0ab287ef-7bd6-4cc3-b53b-c916c857cbe7",
                        "messageId": "33b9fcbf-b326-4eb4-bba6-cf34979f4ba2",
                        "flowRunId": 1607944841843,
                        "quotaId": "qs-4882d03d-65e1-44dc-983e-d2a33071779d"
                    },
                    "content": {
                        "to": [
                            {
                                "email": "martin@client.io",
                                "type": "to"
                            }
                        ],
                        "from_email": "no-reply@appmixer.com"
                    },
                    "scope": {
                        "bcbeda1d-9036-45af-a25d-a57cf06e3f90": {
                            "out": {
                                "started": "2020-12-14T11:20:41.858Z"
                            }
                        }
                    },
                    "originalContent": {
                        "started": "2020-12-14T11:20:41.858Z"
                    }
                }
            ]
        }
    }
    {
      "event": "storage_quota_warning",
      "userId": "5f804b96ea48ec47a8c444a7",
      "username": "user@example.com",
      "totalBytes": 94371840,
      "softLimitBytes": 104857600,
      "hardLimitBytes": 524288000,
      "warningThreshold": 0.9,
      "utilizationPercent": 90.1,
      "timestamp": "2026-04-23T10:00:00Z"
    }
    {
      "event": "storage_quota_soft_exceeded",
      "userId": "5f804b96ea48ec47a8c444a7",
      "username": "user@example.com",
      "totalBytes": 110000000,
      "softLimitBytes": 104857600,
      "hardLimitBytes": 524288000,
      "utilizationPercent": 104.9,
      "timestamp": "2026-04-23T11:00:00Z"
    }
    {
      "event": "storage_quota_hard_exceeded",
      "userId": "5f804b96ea48ec47a8c444a7",
      "username": "user@example.com",
      "totalBytes": 530000000,
      "softLimitBytes": 104857600,
      "hardLimitBytes": 524288000,
      "utilizationPercent": 101.1,
      "blocked": true,
      "timestamp": "2026-04-23T12:00:00Z"
    }
    {
      "event": "system_capacity_warning",
      "physicalCapacityBytes": 107374182400,
      "dataBytes": 85899345920,
      "utilizationPercent": 80.0,
      "maxUtilizationPercent": 95.0,
      "state": "yellow",
      "timestamp": "2026-04-23T10:00:00Z"
    }
    {
      "event": "system_capacity_critical",
      "physicalCapacityBytes": 107374182400,
      "dataBytes": 102005473075,
      "utilizationPercent": 95.0,
      "maxUtilizationPercent": 95.0,
      "state": "red",
      "blockedSubsystems": [
        "inputQueue",
        "dispatcher",
        "webhooks",
        "fileUpload",
        "flowCreate",
        "flowStart",
        "userSignup",
        "dataStore",
        "polling",
        "delayedMessages",
        "componentTimeouts"
      ],
      "timestamp": "2026-04-23T13:00:00Z"
    }

    Error Notification Webhooks

    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.

    Recommendation for End-User Error Notification Content

    Example: Data Validation Error

    Example: Network Error

    Storage Quota Notifications

    Storage Quota Webhook Events

    WEBHOOK_STORAGE_QUOTA_WARNING

    WEBHOOK_STORAGE_QUOTA_SOFT_EXCEEDED

    WEBHOOK_STORAGE_QUOTA_HARD_EXCEEDED

    System Capacity Webhook Events

    WEBHOOK_SYSTEM_CAPACITY_WARNING

    WEBHOOK_SYSTEM_CAPACITY_CRITICAL

    Notification Cooldown

    Use Cases

    . Rather than skipping over results (which gets slower on deeper pages),
    searchAfter
    picks up exactly where the previous page left off.

    How it works:

    1. Make your first request normally (without searchAfter).

    2. From the last log entry in the response, take its sort field value(s) and _id.

    3. Pass those values back as searchAfter parameters in the next request.

    4. Repeat for each subsequent page.

    Important: The number of searchAfter values must match the number of sort parameters. Each searchAfter value corresponds to a sort field in the same order. If they don't match, the API returns an error.

    Example — paginating with gridTimestamp and _id as a tiebreaker:

    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

    Here, searchAfter=2023-10-25T15:03:17.721Z matches sort=gridTimestamp:asc and searchAfter=wC5cZ4sB5dpG2lX0gmxi (the _id of the last log) matches sort=_id:asc. Using _id as a secondary sort ensures no duplicates or missed records when multiple logs share the same timestamp.

    Name
    Type
    Description

    portType

    string

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

    flowId

    string

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

    GET [TENANT_API_URL]/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]"

    Name
    Type
    Description

    logId

    string

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

    logIndex

    string

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

    POST [TENANT_API_URL]/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.

    Name
    Type
    Description

    flowId

    string

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

    excludes

    string

    Name
    Type
    Description

    aggs

    object

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

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

    GET [TENANT_API_URL]/telemetry

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

    Name
    Type
    Description

    to

    string

    To date.

    from

    string

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

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

    Name
    Type
    Description

    to

    string

    To date.

    from

    string

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

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

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

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

    Get Logs and Histogram

    Pagination with searchAfter

    Query Parameters

    Get Log Detail

    Path Parameters

    Get Logs (Aggregations)

    Query Parameters

    Request Body

    Get Usage Information for Current User

    Query Parameters

    Get Usage Information For Other Users

    Query Parameters

    Get Flow Usage Information

    this section
    inPorts
    Providing test data with test(context)

    Flow Manager

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

    Flow Manager

    Configuration

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

    const flowManager = appmixer.ui.FlowManager(config)
    
    flowManager.set(key, value)
    flowManager.get(key)

    config.el ...

    Learn about widget config .

    config.options

    Type: Object | Default: DefaultOptions

    config.options.menu

    Type: Object[] | Default: []

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

    Type: Object | Default: DefaultShareTypes

    Override default sharing dialog types.

    Type: Object[] | Default: DefaultSharePermissions

    Override default sharing dialog permissions.

    Type: Object[] | Default: []

    Create dropdown inputs with built-in query filters:

    Filters clearly indicate when they are applied and can easily be reset (this applies to both the Flow Manager and the Integrations manager, in both grid and list layouts):

    • Each filter dropdown renders as a chip with a Group: value label (e.g. Stage: Running flows). An applied filter is highlighted, so flows can never be "lost" behind an invisible filter.

    • An active chip shows an × button that clears just that filter group (in list layout, the × appears next to the column-header dropdown).

    • Every filter menu offers an All option at the top that clears the group, and the active option is marked with a checkmark.

    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:

    Type: Object[] | Default: []

    Create dropdown inputs with built-in sorting:

    Type: Boolean | Default: null

    Toggle a custom loading state.

    Type: String | Default: null

    Toggle a custom error message.

    Type: String | Default: grid

    Change layout of the widget.

    Type: Object | Default: DefaultQuery

    Set custom query parameters.

    Select a flow to open in Designer widget.

    Click Create Flow button.

    Toggle flow stage button.

    Toggle flow stage button.

    Click menu item to clone a flow.

    Click menu item to open sharing of a flow.

    Click menu item to rename flow.

    Click menu item to remove a flow.

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

    Custom Theme

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

    Basic usage

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

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

    and/or use the option with individual widgets:

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

    Usage with multiple themes

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

    // change the theme of all widgets
    appmixer.set('theme', {
        variables: {
            font: {
                family: 'serif'
            }
        }
    })
    
    // or/and change the theme of a single widget
    widget.set('theme', {
        variables: {
            colors: {
                neutral: 'green'
            }
        }
    });

    Variables

    The easiest way to change the overall styling is to use the theme variables. The following example shows a complete list of variables that you can set in your theme to match your product branding:

    appmixer.set('theme', {
        mode: 'light', // Determines the color mode of the theme: 'light' or 'dark'.
        variables: {
            // Font variables including font family, weights, and size.
            font: {
                family: '\'SF Pro Text\', \'Helvetica Neue\', \'Helvetica\', \'Arial\', sans-serif',
                familyMono: '\'SF Mono\', \'ui-monospace\', Menlo, monospace',
                weightRegular: 400,
                weightMedium: 500,
                weightSemibold: 600,
                weightBold: 700, // Added key for bold font weight.
                size: 14
            },
            // Color variables for various UI elements, with light and dark mode defaults.
            colors: {
                background: '#FFFFFF', // Color of the background. The areas accomodate surfaces.
                surface: '#FFFFFF', // Color of the surfaces above background and other surfaces.
                separator: '#E0E0E2', // Separator is a special color for various borders and lines.
                neutral: '#1F2338',
                primary: '#2A64F6', // Colors for primary, secondary and tertiary actions of the user.
                secondary: '#6B7EB3',
                tertiary: '#8C6C87',
                error: '#B3261E',
                warning: '#B56C09',
                success: '#08B685',
                modifier: '#C558CF', // Special color for variables that have been modified via Modifiers.
                highlighter: '#FFA500'
            },
            // Shadow variables for different elevation levels and UI elements.
            shadows: {
                level0: '0 0 4px 0 rgba(60, 64, 67, 0.3)', // Shadows from level 0 to 5 serve to assist with elevation levels between surfaces.
                level1: '0 0 1px rgb(125 125 126)',
                level2: '0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.12)',
                level3: '0 2px 4px 0 rgba(60, 64, 67, 0.3), 0 2px 6px 1px rgba(60, 64, 67, 0.12)',
                level4: '0 3px 9px 0 rgba(60, 64, 67, 0.15), 0 3px 12px 1px rgba(60, 64, 67, 0.12)',
                level5: '0 4px 8px 0 rgba(60, 64, 67, 0.3), 0 4px 12px 1px rgba(60, 64, 67, 0.12)',
                backdrop: 'rgba(0 0 0 / 88%)', // Backdrop is a special shadow to cover the background of modals and popups.
                blur: 'rgba(0 0 0 / 10%)', // Blur is an alternative for "backdrop" when the background is blurred instead.
                popover: '0 3px 9px rgba(0 0 0 / 12%)', // Popover is a special shadow for popups that should be at the maximum elevation.
                icon: '0 1px 3px rgb(0 0 0 / 6%)'
            },
            // Radius variables for element and container corners.
            corners: {
                elementRadiusSmall: '3px',
                elementRadiusMedium: '6px',
                elementRadiusLarge: '9px',
                elementRadiusRound: '300px',
                containerRadiusSmall: '3px',
                containerRadiusMedium: '6px',
                containerRadiusLarge: '9px'
            },
            // Border width variables for UI elements.
            dividers: {
                regular: '1px', // Width of border lines and separator lines in the UI.
                medium: '2px',
                semibold: '3px',
                bold: '6px',
                extrabold: '9px'
            }
        }
    })

    Shapes

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

    appmixer.set('theme', {
        ui: {
            shapes: {
                action: "action",
                trigger: "trigger",
                selection: "selection"
            }
        }
    })

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

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

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

    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.

    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.

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

    Appmixer widgets render thin, theme-coloured scrollbars instead of the browser's native ones. The difference is most visible on Windows, where native scrollbars are wide and grey. The thumb colour comes from the theme's scrollbarColor property and defaults to neutral32 over a transparent track — it is derived from the palette, so a custom theme adapts automatically.

    Because scrollbar-color is an inherited CSS property, it is set on the root that anchors each part of the UI. There are three such roots, and a theme that overrides only some of them leaves the rest unthemed:

    Theme path
    Applies to

    The third one is a top-level scope, not a mixin: teleported overlays render outside the widget subtree, so neither mixin can reach them.

    The colour can also be set per widget, for example under '#Designer'. Setting scrollbarColor: 'auto' restores the browser's native thumb colour, and the native width can be restored from the host page:

    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:

    Automation Hub

    API for Automation Hub configuration and shareable link management

    The Automation Hub API provides endpoints for managing hub settings, analytics, and shareable link authentication for public marketplace access.

    The Automation Hub API includes three main areas:

    • Settings: Configure hub appearance, behavior, and access controls (admin only)

    • Analytics: Track template usage and performance metrics (see )

    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.

    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

    Cursor-based pagination parameter, more efficient than from for deep pagination. Pass the sort value(s) and _id of the last document from the previous page. The number of searchAfter values must match the number of sort parameters. It is recommended to always use _id as a secondary sort to avoid duplicates. See the Pagination with searchAfter section above for details.

    testRunId

    string

    Return only records produced by the given flow test run. Accepts a single value or repeats it (testRunId=a&testRunId=b).

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

    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

    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

    Cursor-based pagination parameter, more efficient than from for deep pagination. Pass the sort value(s) and _id of the last document from the previous page. The number of searchAfter values must match the number of sort parameters. It is recommended to always use _id as a secondary sort to avoid duplicates. See the Pagination with searchAfter section above for details.

    testRunId

    string

    Return only records produced by the given flow test run. Accepts a single value or repeats it (testRunId=a&testRunId=b).

    From date.

    From date.

    userId

    string

    A user ID.

    Searching Records
    Searching Records
    Searching Records
    Searching Records

    selection-vertical

    selection-dark

    selection-vertical-dark

    action

    action-vertical

    action-dark

    action-vertical-dark

    trigger

    trigger-vertical

    trigger-dark

    trigger-vertical-dark

    appmixer.ui.FlowManager({
        el: '#app',
        theme: {
            ui: {
                '#FlowManager': {
                    background: 'lightblue',
                    '#header': {
                        padding: '0 0 24px 0',
                        '#buttonCreateFlow': {
                            color: 'yellow',
                            '@hovered': {
                                color: 'white'
                            }
                        }
                    }
                }
            }
        }
    });

    ui.mixins['#container'].scrollbarColor

    Scrollable areas inside a normal widget.

    ui.mixins['#widgetModal'].scrollbarColor

    Widgets rendered inside a modal, which have no #container ancestor.

    ui['#scrollbars'].scrollbarColor

    appmixer.set('theme', {
        ui: {
            '#scrollbars': {
                scrollbarColor: 'rgba(0, 0, 0, 0.4) transparent'
            },
            mixins: {
                '#container': {
                    scrollbarColor: 'rgba(0, 0, 0, 0.4) transparent'
                },
                '#widgetModal': {
                    scrollbarColor: 'rgba(0, 0, 0, 0.4) transparent'
                }
            }
        }
    });
    .am-widget * { scrollbar-width: auto; }
    wget  https://my.appmixer.com/appmixer/package/theme-light.json
    wget  https://my.appmixer.com/appmixer/package/theme-dark.json

    The -vertical presets select the top-to-bottom layout, which keeps the Designer on the previous diagram — the new diagram supports left-to-right layouts only. See Designer → Diagram.

    action/trigger

    action-vertical/trigger-vertical

    action-dark/trigger-dark

    action-vertical-dark/trigger-vertical-dark

    Advanced UI Styling

    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.

    Colors

    Font

    Scrollbars

    If your host application styles ::-webkit-scrollbar inside Appmixer widgets, those rules no longer apply in Chromium. Setting the standard scrollbar-width / scrollbar-color properties switches Chrome and Edge to the standard scrollbar rendering path, where the ::-webkit-scrollbar pseudo-elements are ignored. Migrate such styling to the standard properties, or neutralise the SDK defaults as shown above. Safari before 18.2 still renders native scrollbars.

    Complete Theme Object

    FlowManager Dark Theme
    Designer Dark Theme
    Insights Logs Dark Theme

    selection

    Overlays teleported to document.body — modals, popovers, the Automation Hub dialog, the flow logs popover and the field editor overlay.

    Shareable Links: Enable public access via email-based authentication with OTP tokens

    GET http://[API-URL]/automation-hub/settings

    Retrieve the current Automation Hub settings. All authenticated users can read settings, but sensitive fields (like email allow lists) are filtered for non-admin users.

    {
      "id": "default
    
    {
      "id": "default
    

    Field Filtering:

    • Admin users: See all settings including sharing.allowedEmails

    • Regular users: Sensitive fields are automatically filtered from the response

    POST http://[API-URL]/automation-hub/settings

    Create or update Automation Hub settings. This endpoint performs an upsert operation. Admin token required.

    Name
    Type
    Description

    settings*

    object

    Settings object with flexible schema

    settings.sharing.allowedEmails

    array

    {
      "id": "default",
      "settings": {
        "sharing": {
          "allowedEmails": ["user@example.com", "team@company.com"]
        },
        "theme": {
          "primaryColor": "#007bff"
        }
      },
      "createdAt": "2026-01-15T10:30:00.000Z",
      "updatedAt": "2026-04-22T09:15:00.000Z"
    }
    {
      "statusCode": 400,
      "error": "Bad Request",
      "message": "\"settings.sharing.allowedEmails[0]\" must be a valid email"
    }
    {
      "statusCode": 401,
      "error": "Unauthorized",
      "message": "Missing authentication"
    }
    {
      "statusCode": 403,
      "error": "Forbidden",
      "message": "Insufficient scope"
    }

    DELETE http://[API-URL]/automation-hub/settings

    Delete all Automation Hub settings and reset to defaults. Admin token required.

    Settings successfully deleted.

    {
      "statusCode": 401,
      "error": "Unauthorized",
      "message": "Missing authentication"
    }
    {
      "statusCode": 403,
      "error": "Forbidden",
      "message": "Insufficient scope"
    }

    Shareable links allow you to provide public access to your Automation Hub marketplace using email-based authentication with one-time passwords (OTP). Users receive activation links via email to authenticate without traditional login credentials.

    1. Email Allow List: Admin configures allowed emails in hub settings

    2. Request Login: User requests access with their email

    3. Activation Link: System sends time-limited activation code

    4. Token Generation: User activates link to receive JWT token with OTP flag

    5. Virtual User: System creates virtual user account automatically

    POST http://[API-URL]/automation-hub/share/request-login

    Request an activation link for email-based login. No authentication required. Rate limited by email and IP address.

    Name
    Type
    Description

    email*

    string

    Email address (max 255 characters)

    {
      "success": true,
      "message": "Activation link sent to your email"
    }

    Email will be sent automatically with the activation link.

    {
      "success": true,
      "code": "a1b2c3d4e5f6",
      "link": "https://your-hub.com/automation-hub/share/activate?code=a1b2c3d4e5f6",
      "expiresIn": 3600
    }

    When AUTOMATION_HUB_AUTO_SEND_EMAIL is disabled, the link is returned directly.

    {
      "error": "Email not authorized. Please contact your administrator."
    }

    The email is not on the allow list configured in hub settings.

    {
      "statusCode": 429,
      "error": "Too Many Requests",
      "message": "Too many requests for this email address. Please try again in a few minutes."
    }

    Rate limit exceeded. Separate limits apply per email and per IP address.

    {
      "error": "Failed to send activation email. Please try again."
    }

    Email sending failed (SMTP or Cloud Email API error).

    Rate Limits:

    • Per email: Configurable via AUTOMATION_HUB_RATE_LIMIT_REQUEST_LOGIN_EMAIL (default: 5 requests per minute)

    • Per IP: Configurable via AUTOMATION_HUB_RATE_LIMIT_REQUEST_LOGIN_IP (default: 20 requests per minute)

    Email Delivery: The system automatically selects the email service:

    • If SMTP_HOST is configured, uses SMTP

    • Otherwise, uses Cloud Email API (requires APPMIXER_CLOUD_EMAIL_API and APPMIXER_CLOUD_EMAIL_API_KEY)

    GET http://[API-URL]/automation-hub/share/activate?code=[CODE]

    Activate the link, create or find virtual user, and return JWT token. No authentication required. Rate limited by IP address.

    Name
    Type
    Description

    code*

    string

    Activation code from email link

    {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "user": {
        "id": "507f1f77bcf86cd799439011",
        "email": "user@example.com",
        "username": "user@example.com"
      }
    }

    The JWT token includes an otp: true flag and can be used for authentication.

    {
      "error": "Activation link has already been used"
    }

    Each activation link can only be used once.

    {
      "error": "Invalid or expired activation link"
    }

    The activation code is invalid or the link has expired.

    {
      "statusCode": 429,
      "error": "Too Many Requests",
      "message": "Too many activation attempts. Please try again in a few minutes."
    }

    Rate limit exceeded for IP address.

    Rate Limits:

    • Per IP: Configurable via AUTOMATION_HUB_RATE_LIMIT_ACTIVATE_IP (default: 10 requests per minute)

    Virtual Users:

    • Automatically created on first activation

    • Username and email set to the activation email address

    • Flagged as virtual users in the system

    • Can access only the Automation Hub marketplace

    POST http://[API-URL]/automation-hub/share/refresh

    Request a new activation link for users with expired tokens. Works exactly like /request-login but intended for users who already have an expired session. No authentication required. Rate limited by email and IP address.

    Name
    Type
    Description

    email*

    string

    Email address (max 255 characters)

    Rate Limits:

    • Per email: Configurable via AUTOMATION_HUB_RATE_LIMIT_REFRESH_EMAIL (default: 5 requests per minute)

    • Per IP: Configurable via AUTOMATION_HUB_RATE_LIMIT_REFRESH_IP (default: 20 requests per minute)

    SMTP (Option 1):

    Cloud Email API (Option 2):

    The system automatically uses SMTP if SMTP_HOST is set, otherwise falls back to Cloud Email API.

    Enable public users to access your Automation Hub marketplace:

    Configure custom theme and branding:

    Dynamically manage email access:

    The email allow list provides the primary access control mechanism:

    • Only emails on the list can request activation links

    • Case-insensitive matching

    • Constant-time response prevents email enumeration attacks

    • Administrators manage the list via settings API

    Multiple rate limits protect against abuse:

    • Per Email: Prevents spam to specific addresses

    • Per IP: Prevents brute-force attacks from single source

    • Separate Limits: Different limits for request, activate, and refresh operations

    Activation links include several security features:

    • Time-Limited: Configurable TTL (default: 1 hour)

    • Single-Use: Each code can only be activated once

    • Atomic Marking: Race condition protection prevents double-use

    • Random Codes: Cryptographically secure token generation

    Virtual users have restricted capabilities:

    • Created automatically on activation

    • Limited to Automation Hub marketplace access

    • Separated from regular user accounts

    • JWT tokens include otp: true flag for identification

    All hub settings operations are automatically logged:

    Settings Update:

    Settings Delete:

    Each audit log entry includes:

    • Operation type (upsert/delete)

    • Before and after state

    • Timestamp and admin user who performed the action

    • Full settings object (including sensitive fields)

    See Audit Logs for more information.

    Email Not Authorized (403)

    Solution: Add email to allow list via settings API

    Rate Limit Exceeded (429)

    Solution: Wait 60 seconds before retrying

    Invalid Activation Code (404)

    Solution: Request new activation link via /request-login or /refresh

    Link Already Used (400)

    Solution: Request new activation link - each code can only be used once

    If email sending fails:

    1. Check SMTP configuration (host, port, credentials)

    2. Verify Cloud Email API configuration (URL, API key)

    3. Review server logs for detailed error messages

    4. Ensure firewall allows outbound email connections

    Overview

    Analytics API
    curl -XGET "http://[API-URL]/automation-hub/settings" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    curl -XPOST "http://[API-URL]/automation-hub/settings" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "settings": {
          "sharing": {
            "allowedEmails": ["user@example.com", "team@company.com"]
          },
          "theme": {
            "primaryColor": "#007bff"
          }
        }
      }'
    curl -XDELETE "http://[API-URL]/automation-hub/settings" \
      -H "Authorization: Bearer [ADMIN_TOKEN]"
    curl -XPOST "http://[API-URL]/automation-hub/share/request-login" \
      -H "Content-type: application/json" \
      -d '{ "email": "user@example.com" }'
    curl -XGET "http://[API-URL]/automation-hub/share/activate?code=a1b2c3d4e5f6"
    curl -XPOST "http://[API-URL]/automation-hub/share/refresh" \
      -H "Content-type: application/json" \
      -d '{ "email": "user@example.com" }'
    SMTP_HOST=smtp.example.com
    SMTP_PORT=587                          # Default: 587
    SMTP_USER=your-username
    SMTP_PASS=your-password
    MAILER_FROM_NAME=Appmixer              # Default: Appmixer
    MAILER_FROM_EMAIL=noreply@appmixer.com # Default: noreply@appmixer.com
    APPMIXER_CLOUD_EMAIL_API=https://api.appmixer.com/email
    APPMIXER_CLOUD_EMAIL_API_KEY=your-api-key
    # Control email sending behavior
    AUTOMATION_HUB_AUTO_SEND_EMAIL=true    # Default: true
    
    # Activation link TTL (time-to-live) in seconds
    AUTOMATION_HUB_ACTIVATION_LINK_TTL=3600  # Default: 3600 (1 hour)
    
    # Custom activation URL (optional)
    AUTOMATION_HUB_ACTIVATE_URL=https://your-domain.com/activate
    # If not set, uses: ${APPMIXER_FE_URL}/automation-hub/share/activate
    # Request login limits
    AUTOMATION_HUB_RATE_LIMIT_REQUEST_LOGIN_EMAIL=5   # Default: 5 per minute
    AUTOMATION_HUB_RATE_LIMIT_REQUEST_LOGIN_IP=20     # Default: 20 per minute
    
    # Activate link limits
    AUTOMATION_HUB_RATE_LIMIT_ACTIVATE_IP=10          # Default: 10 per minute
    
    # Refresh link limits
    AUTOMATION_HUB_RATE_LIMIT_REFRESH_EMAIL=5         # Default: 5 per minute
    AUTOMATION_HUB_RATE_LIMIT_REFRESH_IP=20           # Default: 20 per minute
    # 1. Configure email allow list (admin)
    curl -XPOST "http://[API-URL]/automation-hub/settings" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "settings": {
          "sharing": {
            "allowedEmails": ["partner@company.com", "customer@example.com"]
          }
        }
      }'
    
    # 2. User requests access (no auth)
    curl -XPOST "http://[API-URL]/automation-hub/share/request-login" \
      -H "Content-type: application/json" \
      -d '{ "email": "partner@company.com" }'
    
    # 3. User receives email, clicks link and activates
    curl -XGET "http://[API-URL]/automation-hub/share/activate?code=a1b2c3d4e5f6"
    
    # Returns JWT token for marketplace access
    curl -XPOST "http://[API-URL]/automation-hub/settings" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "settings": {
          "theme": {
            "primaryColor": "#007bff",
            "logoUrl": "https://example.com/logo.png",
            "companyName": "Acme Corp"
          },
          "marketplace": {
            "welcomeMessage": "Welcome to our integration marketplace",
            "categories": ["CRM", "Marketing", "Sales"]
          }
        }
      }'
    # Get current settings
    CURRENT=$(curl -s -XGET "http://[API-URL]/automation-hub/settings" \
      -H "Authorization: Bearer [ADMIN_TOKEN]")
    
    # Add new email to allow list
    curl -XPOST "http://[API-URL]/automation-hub/settings" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "settings": {
          "sharing": {
            "allowedEmails": [
              "existing@example.com",
              "new-user@example.com"
            ]
          }
        }
      }'
    Automation hub settings updated.
    Automation hub settings deleted.
    {
      "error": "Email not authorized. Please contact your administrator."
    }
    {
      "statusCode": 429,
      "error": "Too Many Requests",
      "message": "Too many requests for this email address. Please try again in a few minutes."
    }
    {
      "error": "Invalid or expired activation link"
    }
    {
      "error": "Activation link has already been used"
    }

    Hub Settings

    Get Settings

    Update Settings

    Request Body

    Flexible Settings Schema

    The settings object supports any properties, allowing you to add custom fields without backend changes. Only settings.sharing.allowedEmails has validation (must be array of valid email addresses).

    Delete Settings

    Shareable Links

    Architecture

    Request Login

    Request Body

    Activate Link

    Query Parameters

    Refresh Link

    Request Body

    Refresh vs Request Login

    Both endpoints function identically but serve different use cases:

    • /request-login: Initial access request

    • /refresh

    Configuration

    Environment Variables

    Email Configuration

    Activation Link Settings

    Rate Limiting

    Use Cases

    Public Marketplace Access

    Custom Hub Theming

    Programmatic Allow List Management

    Security

    Email Allow List

    Rate Limiting

    Activation Link Security

    Virtual Users

    Audit Logging

    Error Handling

    Common Errors

    Email Sending Failures

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

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

    config.options.shareTypes

    config.options.sharePermissions

    config.options.filters

    config.options.customFilter

    config.options.sorting

    Instance

    Learn about widget instance here.

    State

    loader

    error

    layout

    query

    Events

    flow:open

    flow:create

    flow:start

    flow:stop

    flow:clone

    flow:share

    flow:rename

    flow:remove

    Sharing

    Example

    here
    Flow Manager Menu
    Flow Manager Filters
    Flow Manager Sorting
    Flow Manager Sharing

    Example: Webhook Trigger

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

    Demo Todo API

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

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

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

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

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

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

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

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

    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.

    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.

    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

    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

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

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

    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.

    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.

    AI Copilot

    AI Copilot is an assistant built into the Appmixer Designer that builds, edits and explains flows from natural language. Ask it to "Create a flow that posts new Typeform responses to Slack" and it designs the flow directly on the canvas — selecting connectors, wiring components, and filling in parameters.

    Copilot can:

    • Build new flows from a natural-language description.

    • Edit existing flows — add, replace or remap components in the open flow. For running flows, Copilot works on a draft so the live flow keeps running unchanged.

    Apps

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

    GET [TENANT_API_URL]/apps

    Returns all applications available to the authenticated user. An "app" is either a module (e.g. appmixer.google.gmail) or a service that does not declare any modules (e.g. appmixer.calendly). When a service contains at least one module, the modules are returned as the apps and the service itself is omitted. The result is keyed by the app name and respects the user's ACL (private apps the user cannot see are filtered out). curl "https://api.appmixer.com/apps" -H "Authorization: Bearer [ACCESS_TOKEN]"

    : Token renewal for existing users

    Both share the same allow list validation and rate limiting.

    "
    ,
    "settings": {
    "sharing": {
    "allowedEmails": [ // Only visible to admin users
    "user@example.com",
    "team@company.com"
    ]
    },
    "theme": {
    "primaryColor": "#007bff",
    "logoUrl": "https://example.com/logo.png"
    },
    "customField": "any-value" // Settings support flexible schema
    },
    "createdAt": "2026-01-15T10:30:00.000Z",
    "updatedAt": "2026-04-20T14:45:00.000Z"
    }
    "
    ,
    "settings": {},
    "createdAt": null,
    "updatedAt": null
    }

    Email addresses allowed to access Hub via share link

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

    {{config.baseUrl}}
    . The
    {{config.CONFIG_KEY}}
    allow us to use service configuration values defined in the Backoffice admin panel. This is very handy if you don't want to hardcode certain values in your service/module/component definition and instead, make those values configurable via Backoffice. To do that, visit your Appmixer tenant Backoffice interface, go to "Services", click "Add" to add a new service configuration, provide the correct service ID in the form
    [vendor]:[service]
    and add your custom configuration key/values by clicking on the "burger" icon:
    field points to our authentication module and must be of the form
    [vendor].[service]
    (or
    [vendor].[service].[module]
    in case the
    auth.js
    authentication module is defined under the module directory (which is not our case).

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

    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 <info@appmixer.com>",
        "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQA...",
        "webhook": true,
        "auth": {
            "service": "appmixer:tododemo"
        },
        "outPorts": [
            {
                "name": "out",
                "options": [
                    { "label": "Todo ID", "value": "id" },
                    { "label": "Todo Label", "value": "label" }
                ]
            }
        ]
    }
    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 [TENANT_API_URL] # or any other custom endpoint in case of self-managed Appmixer installation
    $ appmixer login your-admin-user@example.com # note the user must have the "appmixer" vendor set in Backoffice
    $ appmixer pack tododemo/
    $ appmixer publish appmixer.tododemo.zip

    Appmixer Service Definition

    Service Manifest (service.json)

    Service Authentication Module

    Component manifest (component.json)

    Component Behaviour (NewTodo.js)

    Packing and Publishing our Service

    Download the demo Service

    https://docs.appmixer.com/appmixer/appmixer-self-managed/installation#enabling-users-to-publish-custom-components
    https://en.wikipedia.org/wiki/Data_URI_scheme
    https://docs.appmixer.com/appmixer/component-definition/authentication#authentication-module-structure
    https://github.com/axios/axios#request-config
    https://docs.appmixer.com/appmixer/component-definition/behaviour
    https://docs.appmixer.com/appmixer/appmixer-cli/appmixer-cli
    appmixer.tododemo.zip
    archive · 7KB
    Open

    Explain flows — describe what an existing flow does, step by step.

    Copilot ships as a system plugin of the Appmixer engine. It is loaded when it is listed in the SYSTEM_PLUGINS environment variable and an LLM API key is configured:

    Additional configuration:

    Environment Variable
    Default
    Description

    COPILOT_LLM_PROVIDER

    anthropic

    LLM provider.

    COPILOT_LLM_API_KEY

    Copilot's configuration lives in the cluster configuration and can be edited from the Backoffice. Changes apply without restarting a pod.

    When the plugin is not enabled, the Copilot button is automatically hidden in the Designer.

    Some LLM gateways need a header on every request to identify the installation, the tenant or the end user behind it. Copilot can add such headers in two ways. Both are unset by default — nothing changes for a deployment that does not configure them — and both live in the cluster configuration, so they can be edited from the Backoffice without restarting a pod.

    • COPILOT_FORWARDED_HEADERS forwards headers from the incoming chat request, for a value that differs per user or per request.

    • COPILOT_LLM_HEADERS adds static headers to every LLM request, for a value that is the same for the whole deployment.

    Both apply to whichever endpoint Copilot talks to: the gateway named by COPILOT_LLM_URL, the Appmixer Cloud proxy, or the provider's own API when neither is set. The static headers are also sent with the model availability check Copilot runs at startup.

    COPILOT_FORWARDED_HEADERS is a comma-separated allow-list of header names, matched case-insensitively. A trailing * matches every header with that prefix:

    Only headers named on the list are forwarded from POST /system-plugins/copilot/chat to the LLM request. A bare * is not a wildcard — the Backoffice rejects it, as it does any entry that is not a valid header name. An entry that can match no header is reported in a warning on every chat request.

    COPILOT_LLM_HEADERS is a JSON object of headers added to every LLM request:

    Being administrator data, it may carry an Authorization header for a gateway that authenticates with a bearer token. On the Appmixer Cloud proxy path that would replace the proxy's own token, so set it only for a direct gateway.

    When both settings carry the same header, the static value wins, so a client can never override an administrator-pinned header. A malformed value in the environment is reported and ignored rather than stopping the node; the Backoffice rejects one up front.

    Any holder of a valid access token can call the chat endpoint directly with headers of their choosing, so the forwarded values are treated as untrusted input. Whatever the allow-list matches, these headers are never placed on an LLM request:

    • Credential headers — Authorization, Proxy-Authorization, api-key, x-api-key, Cookie, Set-Cookie. Authorization is the one exception for static headers, as described above; a forwarded header can never carry it.

    • Transport, framing and encoding headers — Host, Connection, Content-Length, Content-Type, Content-Encoding, Accept-Encoding, Transfer-Encoding, TE, Trailer, Upgrade, Keep-Alive, Expect.

    • Headers the provider SDK owns — anthropic-version, anthropic-beta, User-Agent.

    Further limits apply to every header from either source:

    • Values must be printable ASCII. A value carrying a line break is dropped rather than failing the whole chat request.

    • A value is capped at 4096 characters.

    • At most 32 headers are forwarded per request.

    • Only header names are logged, never values, and the values are kept out of the Langfuse traces.

    COPILOT_LLM_REFUSED_HEADERS_EXCEPTIONS lifts individual names from that list, for a gateway that needs a header the list cannot anticipate:

    COPILOT_CUSTOM_CONTEXT applies to everyone on the instance. When different users need different context, each user can also carry their own in the optional aiContext field on the user object. Copilot reads it on every chat request and injects it into the system prompt, so its answers and the flows it builds can take account of who it is helping — their timezone or locale, or the group, property or account that a user stands for.

    It is aimed at embedded deployments that provision virtual users through the admin API, where a single Appmixer user often represents a whole customer, site or tenant.

    aiContext is written through the User API, either when the user is created:

    or later, on an existing user:

    Send null or an empty string to clear it:

    The field is returned by GET /user, GET /users/:userId and GET /users, so a user can read the context that describes them.

    • The value is capped at USER_AI_CONTEXT_MAX_LENGTH characters, 8000 by default. The API rejects a longer value outright; Copilot also trims when reading, so an oversized stored value can never bloat the prompt.

    • Like COPILOT_CUSTOM_CONTEXT, the text is injected as a delimited, lower-authority User AI Context section. It can inform Copilot's answers, but cannot change its core role, override its safety or flow-building rules, or make it reveal the system prompt.

    • The field is optional and backward compatible: a user without it produces exactly the same prompt as before the field existed.

    Open a flow in the Designer and click the Copilot button to open the chat panel. Type what you want to build or change. Copilot streams its progress, and when it produces a flow, the canvas is updated and reloaded.

    • The conversation is retained per flow for the duration of your session, and restored when you re-open the panel.

    • Replies are rendered as Markdown.

    • The panel is resizable — drag its edge to change the width, or double-click the resize handle to reset it to the default width.

    • When editing a running flow, Copilot's changes are applied to a draft — publish the draft to apply them (see Flow Versioning).

    When embedding the Designer via the Appmixer UI SDK, the Copilot panel can be configured with the options.copilot object:

    The Insights menu in Appmixer Studio contains two sections:

    • Flow activity — the flow execution logs (previously the "Insights" page).

    • AI usage — Copilot consumption and cost monitoring. This section is visible to administrators only.

    The AI usage page shows:

    • Summary cards with the total spend, the number of requests and the total tokens consumed in the selected period, including an input/output token breakdown.

    • Daily consumption for the selected month.

    • A paged breakdown table grouped by user, group or scope, with free-text search. Expanding a row drills down into the individual LLM requests (or the users within a group/scope), also paged.

    The page is backed by the usage API described below.

    All Copilot endpoints live under the /system-plugins/copilot prefix and require a valid access token.

    POST [TENANT_API_URL]/system-plugins/copilot/chat

    Send a chat message. The response is a Server-Sent Events (SSE) stream.

    Name
    Type
    Description

    message*

    string

    The user's message.

    threadId

    string

    connected (carries the threadId), text-delta (streamed assistant text), tool-call / tool-result / tool-error (progress of the assistant's tool use), flow-result (the generated flow descriptor with flowName, optional followUpQuestions and an optional validationUnavailable flag, set when flow validation could not be reached — clients can use it to warn that the flow was not validated), done (carries token usage) and error.

    GET [TENANT_API_URL]/system-plugins/copilot/conversations/:threadId

    Return the transcript of a prior conversation ({ threadId, messages: [{ role, content }] }). Users can only read their own threads. Returns an empty message list for a thread the conversation garbage collector has already swept (see COPILOT_CONVERSATION_RETENTION_DAYS).

    GET [TENANT_API_URL]/system-plugins/copilot/enabled

    Returns { "enabled": true } when the Copilot plugin is loaded. Clients use this to decide whether to show Copilot UI.

    GET [TENANT_API_URL]/system-plugins/copilot/usage

    Aggregated LLM usage and cost. Admin only.

    Name
    Type
    Description

    from

    string

    Start of the period (ISO date). Defaults to the start of the current month.

    to

    string

    The response contains the aggregated rows for the requested page, a totalCount with the number of rows in the whole filtered set, and totals that are always computed over the full filtered set — a paged response still describes the whole reporting window, not just the page. Rows are ordered deterministically (request count descending), so pages are stable across requests. When grouping by group or scope, each row also contains a per-user breakdown.

    GET [TENANT_API_URL]/system-plugins/copilot/usage/daily

    Daily consumption totals for a month (month=YYYY-MM, defaults to the current month; optional user, group, scope filters). Admin only.

    GET [TENANT_API_URL]/system-plugins/copilot/usage/user/:userId

    Individual LLM request breakdown for a user (optional from/to). Admins can view any user; non-admin users can only view their own usage.

    Supports the same optional offset/limit paging parameters as the Usage endpoint and returns a totalCount alongside the request rows. Omitting offset and limit returns the complete list. Rows are ordered by timestamp, newest first.

    SYSTEM_PLUGINS=copilot
    COPILOT_LLM_API_KEY=sk-ant-...
    COPILOT_FORWARDED_HEADERS=X-Acme-*,X-Request-Id
    COPILOT_LLM_HEADERS={"X-Acme-Installation": "acme"}
    COPILOT_LLM_REFUSED_HEADERS_EXCEPTIONS=api-key
    curl -XPOST "[TENANT_API_URL]/user" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
            "username": "tower@acme.example",
            "email": "tower@acme.example",
            "password": "[PASSWORD]",
            "aiContext": "Timezone: Europe/Prague. This user represents Acme Tower, a 40-unit residential building. Flows built here usually notify the building manager."
          }'
    curl -XPUT "[TENANT_API_URL]/users/[USER_ID]" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "aiContext": "Timezone: America/New_York. Represents the Brooklyn site." }'
    curl -XPUT "[TENANT_API_URL]/users/[USER_ID]" \
      -H "Authorization: Bearer [ADMIN_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{ "aiContext": null }'
    appmixer.ui.Designer({
        el: '#designer',
        flowId: FLOW_ID,
        options: {
            copilot: {
                // Show the built-in, localized example prompts:
                examplePrompts: true
    
                // — or — provide your own (replaces the defaults):
                // examplePrompts: [
                //     'When a Stripe payment succeeds, add a row to Google Sheets',
                //     'Summarize new Zendesk tickets to Slack every morning'
                // ]
    
                // Omit or set false to keep them hidden (default).
            }
        }
    });

    Enabling Copilot

    COPILOT_CUSTOM_CONTEXT and COPILOT_TONE_OF_RESPONSE are injected as a delimited, lower-authority section of the system prompt. They can refine Copilot's context and phrasing, but cannot override its core role, its safety rules or its flow-building rules, and cannot make it reveal the system prompt.

    LLM gateway headers

    Forwarding request headers

    The headers have to be added server-side, by a proxy in front of Appmixer. A browser cannot send them: the CORS preflight allows a fixed list of headers.

    Static headers

    What is never sent

    An exception weakens the guard for both the static and the forwarded headers. A name lifted here can be set by whoever sends the chat request whenever COPILOT_FORWARDED_HEADERS also matches it — lifting x-api-key on the direct path, for example, would let a caller replace the server's own API key.

    Per-user context

    Setting the field

    Writing aiContext always requires an admin token — including on POST /user, which is open to unauthenticated sign-ups by default. Copilot splices the value into its system prompt, so letting end users set their own would be a prompt-injection vector. A request without the admin scope is rejected with 403 Setting aiContext requires the admin scope.

    Limits and precedence

    Using Copilot in the Designer

    Designer widget options

    Monitoring AI Usage

    Copilot API

    Chat

    Request Body

    SSE events

    Get Conversation

    Enabled Check

    Usage

    Query Parameters

    Response

    Cost is computed from the model pricing table (see COPILOT_PRICING_PATH). Requests made with models that have no pricing entry do not add to the cost — they are reported with their token counts but priced as zero.

    Daily Usage

    User Usage

    Name
    Type
    Description

    ignoreACL

    boolean

    Admin only. When true, returns every app regardless of ACL. Non-admin users receive a 400 error.

    {
        "appmixer.asana": {
    

    GET [TENANT_API_URL]/bundles

    Returns the installed bundle versions. Each connector is packaged as a bundle (defined by its bundle.json); this endpoint reports the currently installed version of every bundle, keyed by the bundle name. It is useful for checking which connector versions are deployed in your tenant. curl "https://api.appmixer.com/bundles" -H "Authorization: Bearer [ACCESS_TOKEN]"

    {
        "appmixer.asana": { "version": "1.2.0" },
        "appmixer.calendly": { "version": "2.0.1" },
        "appmixer.dropbox": { "version": "1.5.3" }
    }

    GET [TENANT_API_URL]/apps/components

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

    Name
    Type
    Description

    app

    string

    Required. ID of an app as defined in service.json or module.json (e.g. appmixer.dropbox or appmixer.google.gmail).

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

    GET [TENANT_API_URL]/components

    Get all available components (including custom ones) that the user is allowed to use. By default the endpoint returns the full manifest of every component. To retrieve only the list of component type names, set manifest=no. When full manifests are returned, the icon field is, by default, replaced by a URL pointing to the icon (instead of an inline base64 data URI) to keep the payload small — set compressIcons=false to keep the original inline icons. curl "https://api.appmixer.com/components" -H "Authorization: Bearer [ACCESS_TOKEN]" curl "https://api.appmixer.com/components?manifest=no" -H "Authorization: Bearer [ACCESS_TOKEN]"

    Name
    Type
    Description

    manifest

    string

    When set to no, the endpoint returns only an array of component type names. Otherwise (default) it returns the full manifest of each component.

    selector

    string

    [
        "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"
    ]
    [
        {
            "name": "appmixer.asana.tasks.CreateStory",
            "author": "David Durman <david@client.io>",
            "description": "Create a story (comment) on a task.",
            "icon": "https://api.appmixer.com/icons/appmixer.asana.tasks.CreateStory",
            "auth": { "service": "appmixer:asana" },
            "inPorts": [ /* ... */ ],
            "outPorts": [ /* ... */ ],
            "properties": { /* ... */ }
        }
        // ... one object per component
    ]

    GET [TENANT_API_URL]/components/:selector

    Download the source files of a component, module or an entire service as a zip archive (application/octet-stream). Compiled code and internal node_modules.zip files are excluded from the archive. The user must own the selected component(s). curl -XGET "https://api.appmixer.com/components/appmixer.myservice" -H "Authorization: Bearer [ACCESS_TOKEN]" -o appmixer.myservice.zip

    Name
    Type
    Description

    selector

    string

    A selector identifying what to download. It can target a whole service (appmixer.myservice), a module (appmixer.myservice.mymodule), a single component (appmixer.myservice.mymodule.MyComponent) or even an individual file (appmixer.myservice.mymodule.MyComponent/MyComponent.js).

    (binary zip archive)
    {
        "error": "There is nothing that corresponds to appmixer.myservice."
    }

    POST [TENANT_API_URL]/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 configured per tenant (10MB by default). 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

    Name
    Type
    Description

    compile

    boolean

    When true, the uploaded component code is compiled (minified) during publishing.

    replaceAll

    boolean

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

    GET [TENANT_API_URL]/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"

    Name
    Type
    Description

    ticket

    string

    Ticket that you got from the POST /component request.

    // Successful upload. No errors:
    {
      "finished": "2020-02-28T15:57:34.549Z"
    }
    
    
    // Upload finished. Errors encountered:
    {
      "finished": "2020-02-28T15:25:39.515Z",
      "err": "Invalid schema for appmixer/utils/service.json",
      "data": [
        {
          "keyword": "required",
          "dataPath": "",
          "schemaPath": "#/required",
          "params": {
            "missingProperty": "label"
          },
          "message": "should have required property 'label'"
        },
        {
          "keyword": "required",
          "dataPath": "",
          "schemaPath": "#/required",
          "params": {
            "missingProperty": "description"
          },
          "message": "should have required property 'description'"
        }
      ]
    }
    {
        "error": "There's no upload under ticket 2e9dd726-2b7f-46f7-bea4-8db7f7175aa8"
    }

    DELETE [TENANT_API_URL]/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"

    Name
    Type
    Description

    selector

    string

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

    Name
    Type
    Description

    doNotRemoveCollections

    boolean

    When true, the database collections associated with the deleted component(s) are preserved instead of being dropped.

    {}

    POST [TENANT_API_URL]/component/:vendor/:service/:module/:component

    Execute a single component outside of a running flow ("static call"). This is the mechanism used internally to resolve dynamic dropdown options in the Inspector, and by the AI Agent to invoke its tools.

    curl -XPOST "https://api.appmixer.com/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{ "componentId": "e25dc901-f92a-46a2-8d29-2573d4ad65e5" }'

    Name
    Type
    Description

    outPort

    string

    The output port whose result should be returned.

    Name
    Type
    Description

    componentId

    string

    ID of a component instance (used to resolve the connected account). Either componentId or callerId is required.

    callerId

    string

    Get Apps

    Query Parameters

    Get Bundles

    Get App Components

    Query Parameters

    Get All Components

    Query Parameters

    Download a Component/Module/Service

    Path Parameters

    Publish A Component/Module/Service

    Query Parameters

    Check for Publishing Progress

    Path Parameters

    Delete a Component/Module/Service

    Path Parameters

    Query Parameters

    Call a Component Statically

    Query Parameters

    Request Body

    Example: twilio.SendSMS

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

    Directory Structure

    twilio/
    ├── auth.js
    ├── package.json
    ├── service.json
    └── sms
        ├── ListFromNumbers
        │   ├── ListFromNumbers.js
        │   ├── component.json
        │   ├── package.json
        │   └── transformers.js
        └── SendSMS
            ├── SendSMS.js
            ├── component.json
            └── package.json

    Component Behaviour (sms/SendSMS/SendSMS.js)

    Defines how the component reacts on incoming messages.

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

    Component Manifest (sms/SendSMS/component.js)

    Defines the component properties and metadata.

    {
        "name": "appmixer.twilio.sms.SendSMS",
        "author": "David Durman <david@client.io>",
        "icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PGcgZmlsbD0iI0NGMjcyRCI+PHBhdGggZD0iTTEyNy44NiAyMjIuMzA0Yy01Mi4wMDUgMC05NC4xNjQtNDIuMTU5LTk0LjE2NC05NC4xNjMgMC01Mi4wMDUgNDIuMTU5LTk0LjE2MyA5NC4xNjQtOTQuMTYzIDUyLjAwNCAwIDk0LjE2MiA0Mi4xNTggOTQuMTYyIDk0LjE2MyAwIDUyLjAwNC00Mi4xNTggOTQuMTYzLTk0LjE2MiA5NC4xNjN6bTAtMjIyLjAyM0M1Ny4yNDUuMjgxIDAgNTcuNTI3IDAgMTI4LjE0MSAwIDE5OC43NTYgNTcuMjQ1IDI1NiAxMjcuODYgMjU2YzcwLjYxNCAwIDEyNy44NTktNTcuMjQ0IDEyNy44NTktMTI3Ljg1OSAwLTcwLjYxNC01Ny4yNDUtMTI3Ljg2LTEyNy44Ni0xMjcuODZ6Ii8+PHBhdGggZD0iTTEzMy4xMTYgOTYuMjk3YzAtMTQuNjgyIDExLjkwMy0yNi41ODUgMjYuNTg2LTI2LjU4NSAxNC42ODMgMCAyNi41ODUgMTEuOTAzIDI2LjU4NSAyNi41ODUgMCAxNC42ODQtMTEuOTAyIDI2LjU4Ni0yNi41ODUgMjYuNTg2LTE0LjY4MyAwLTI2LjU4Ni0xMS45MDItMjYuNTg2LTI2LjU4Nk0xMzMuMTE2IDE1OS45ODNjMC0xNC42ODIgMTEuOTAzLTI2LjU4NiAyNi41ODYtMjYuNTg2IDE0LjY4MyAwIDI2LjU4NSAxMS45MDQgMjYuNTg1IDI2LjU4NiAwIDE0LjY4My0xMS45MDIgMjYuNTg2LTI2LjU4NSAyNi41ODYtMTQuNjgzIDAtMjYuNTg2LTExLjkwMy0yNi41ODYtMjYuNTg2TTY5LjQzMSAxNTkuOTgzYzAtMTQuNjgyIDExLjkwNC0yNi41ODYgMjYuNTg2LTI2LjU4NiAxNC42ODMgMCAyNi41ODYgMTEuOTA0IDI2LjU4NiAyNi41ODYgMCAxNC42ODMtMTEuOTAzIDI2LjU4Ni0yNi41ODYgMjYuNTg2LTE0LjY4MiAwLTI2LjU4Ni0xMS45MDMtMjYuNTg2LTI2LjU4Nk02OS40MzEgOTYuMjk4YzAtMTQuNjgzIDExLjkwNC0yNi41ODUgMjYuNTg2LTI2LjU4NSAxNC42ODMgMCAyNi41ODYgMTEuOTAyIDI2LjU4NiAyNi41ODUgMCAxNC42ODQtMTEuOTAzIDI2LjU4Ni0yNi41ODYgMjYuNTg2LTE0LjY4MiAwLTI2LjU4Ni0xMS45MDItMjYuNTg2LTI2LjU4NiIvPjwvZz48L3N2Zz4=",
        "description": "Send SMS text message through Twilio.",
        "auth": {
            "service": "appmixer:twilio"
        },
        "outPorts": [
            {
                "name": "sent",
                "options": [
                    { "label": "Message Sid", "value": "sid" }
                ]
            }
        ],
        "inPorts": [
            {
                "name": "message",
                "schema": {
                    "type": "object",
                    "properties": {
                        "body": { "type": "string" },
                        "to": { "type": "string" },
                        "from": { "type": "string" }
                    },
                    "required": [
                        "from", "to"
                    ]
                },
                "inspector": {
                    "inputs": {
                        "body": {
                            "type": "text",
                            "label": "Text message",
                            "tooltip": "Text message that should be sent.",
                            "index": 1
                        },
                        "from": {
                            "type": "select",
                            "label": "From number",
                            "tooltip": "Select Twilio phone number.",
                            "index": 2,
                            "source": {
                                "url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
                                "data": {
                                    "transform": "./transformers#fromNumbersToSelectArray"
                                }
                            }
                        },
                        "to": {
                            "type": "text",
                            "label": "To number",
                            "tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
                            "index": 3
                        }
                    }
                }
            }
        ]
    }
    

    Component Dependencies (sms/SendSMS/package.json)

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

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

    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.

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

    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.

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

    Service Authentication Module Dependencies (package.json)

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

    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.

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

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

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

    Restrict the result to a subset of components. A [vendor].[service].[module].[component] selector (e.g. appmixer.google or appmixer.google.gmail).

    selectors[]

    string

    Request multiple component patterns in a single call (OR logic). Example: ?selectors[]=appmixer.google.gmail&selectors[]=appmixer.slack.list.SendChannelMessage. Useful for progressively loading only the manifests a client needs. See also to load the manifests used by a specific flow.

    compressIcons

    boolean

    Default true. When true, the icon field of each manifest is returned as a URL; set to false to return the original inline base64 icon.

    ignoreACL

    boolean

    Admin only. When true, returns every component regardless of ACL. Non-admin users receive a 400 error.

    When true, existing files of the published service/module/component that are not present in the uploaded archive are removed (full replace).

    ID of the calling component instance.

    properties

    object

    Component properties.

    messages

    object

    Input messages keyed by input port.

    log

    boolean

    When true, the call is recorded in the execution logs (marked with staticCall: true) and surfaces in the and the Insights UI — including component identity, input/output payloads, status, error detail and duration. Primarily used for AI Agent tools observability. Default false.

    correlationId

    string

    Optional correlation ID stored in the log record when log is true, so tool-call records can be correlated with the agent's flow run.

    (unset)

    LLM provider API key. The plugin is skipped when no API key is set.

    COPILOT_LLM_MODEL

    claude-sonnet-5

    LLM model used by the assistant.

    COPILOT_LLM_EFFORT

    medium

    Reasoning effort requested from the model on the direct provider path — low, medium, high, xhigh or max. Requested only on models known to accept it; any other model is sent neither field and runs with its own defaults. Thinking tokens are billed as output tokens. Ignored on the Appmixer Cloud proxy path.

    COPILOT_MAX_TOKENS

    (unset)

    Per-step output token ceiling. Unset leaves it to the provider — the model's own maximum for a model id the SDK recognizes, but a conservative 4096 fallback for one it does not, so set it explicitly on a deployment pinned to an aliased or unlisted model.

    COPILOT_MAX_TURNS

    25

    Maximum agent turns per chat request.

    COPILOT_CONVERSATION_RETENTION_DAYS

    0

    How many days a conversation is retained after its last turn. 0 keeps it forever. Expiry is swept periodically by a garbage-collector job, so deletion is not instant. Replaces COPILOT_CONVERSATION_TTL.

    COPILOT_CONVERSATION_MAX_STORED_MESSAGES

    500

    Cap on the messages kept per conversation. The oldest fall off first.

    COPILOT_CONVERSATION_PROMPT_CHAR_BUDGET

    400000

    Cap, in characters, on how much of a conversation is replayed into the prompt on each turn. Storage is unaffected and the history endpoint always returns the full transcript.

    COPILOT_MAX_RUN_TIME

    600000

    Wall-clock cap (ms) on a single chat turn. A turn runs on the server and is no longer aborted by the client disconnecting, so this is what ends one whose provider connection has stalled.

    COPILOT_PRICING_PATH

    (unset)

    Optional path to a custom model pricing JSON file used for cost computation in the usage API.

    COPILOT_LLM_URL

    (unset)

    Base URL for direct LLM provider access, e.g. an Anthropic-compatible gateway. Unset uses the provider SDK default. No effect when the Appmixer Cloud proxy is used.

    COPILOT_APPMIXER_CLOUD_API

    https://cloud.appmixer.com/api/copilot

    Appmixer Cloud proxy endpoint (OpenAI-compatible).

    COPILOT_APPMIXER_CLOUD_API_KEY

    (unset)

    When set, LLM requests are routed through the Appmixer Cloud proxy instead of calling the provider directly.

    COPILOT_FORWARDED_HEADERS

    (unset)

    Comma-separated allow-list of request header names forwarded from the incoming chat request to the LLM request. A trailing * matches a prefix, e.g. X-Acme-*,X-Request-Id. See .

    COPILOT_LLM_HEADERS

    (unset)

    JSON object of static headers added to every LLM request, e.g. {"X-Acme-Installation": "acme"}. See .

    COPILOT_LLM_REFUSED_HEADERS_EXCEPTIONS

    (unset)

    Comma-separated header names lifted from the built-in list of headers that are refused on an LLM request whatever the two settings above say.

    COPILOT_CUSTOM_CONTEXT

    (unset)

    Additional context about your use cases, connectors and data, injected into the system prompt. Up to 16000 characters.

    COPILOT_TONE_OF_RESPONSE

    (unset)

    Tone, vocabulary and register of Copilot's replies. Up to 4000 characters.

    USER_AI_CONTEXT_MAX_LENGTH

    8000

    Maximum length of a single user's aiContext (see ). An engine-level setting rather than a Copilot one: the user API enforces it when the field is written, and Copilot trims to it again when reading.

    COPILOT_VARIABLES_MAX_VARIABLES

    100

    Cap on output variables listed per component when Copilot reads live flow variables.

    COPILOT_VARIABLES_MAX_OPTION_FIELDS

    25

    Cap on inspector fields whose options are read.

    COPILOT_VARIABLES_MAX_OPTIONS_PER_FIELD

    50

    Cap on options read per field.

    COPILOT_VARIABLES_MAX_RESPONSE_CHARS

    24000

    Cap on the whole variables tool result. Raising these shows the model more data at a higher token cost per request.

    INTERNAL_API_URL

    (falls back to APPMIXER_API_URL)

    In-cluster engine URL. Copilot calls the platform API to persist flows while building them; set this so those calls do not route through the external load balancer.

    Conversation thread ID. Omit to start a new conversation (a new ID is generated and returned in the connected event).

    flowId

    string

    Optional ID of the flow to give Copilot as context, enabling editing/explaining of that flow.

    End of the period (ISO date). Defaults to now.

    groupBy

    string

    user (default), group or scope.

    user

    string

    Filter by user ID.

    group

    string

    Filter by group ID.

    scope

    string

    Filter by scope.

    q

    string

    Free-text filter.

    offset

    number

    Optional paging offset. Omit both offset and limit to return the complete result set.

    limit

    number

    Optional page size (must be a positive integer).

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

    Helper Component (sms/ListFromNumbers/ListFromNumbers.js)

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

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

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

    Variables dynamically populated at design time with available Twilio phone numbers.
    LLM gateway headers
    LLM gateway headers
    Per-user context
    Get Flow Components
    Logs API

    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 Appmixer Backoffice 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 support@appmixer.com. Our team can provide guidance on how to effectively set these configuration options and assist with any necessary engine restarts to ensure your configurations are applied as intended.

    Configuration options

    Below is a list of available configuration options, accompanied by a brief explanation for each and their default values. These defaults are used by Appmixer in instances where no specific value is provided:

    Key
    Detail
    Default value
    Needs restart

    GC_FILES_RULES

    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.

    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.

    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

    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 .

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

    1. First, log in to your

    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 .

    AUTH_HUB_AUTOMATIC

    If the auth-hub system plugin is on and this value is true, all OAuth requests for unconfigured services will go through the Authentication Hub. It means that if you install Slack, for example, and do not configure the clientId and clientSecret the engine will use the Appmixer Authentication Hub for Slack authorization.

    true

    DEFAULT_USER_VENDOR

    Vendor assigned to newly created users.

    No value

    AUTH_POPUP_DISPLAY_ERR

    Whether to display 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

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

    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 for more informations.

    No value

    IDP_ROLES_PATH

    Identity provider configuration. Please see for more informations.

    No value

    IDP_DEFAULT_REDIRECT

    Identity provider configuration. Please see for more informations.

    No value

    client_id
    : An identifier for your client application.

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

    Amazon IAM

    -

    Choose a name for your app and note all the details provided by Google; we will need them later.
  • 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.

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

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

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

  • Go to Directory -> Users -> More options -> Manage custom attributes

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

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

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

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

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

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

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

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

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

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

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

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

    Keycloak

    realm_access.roles

    Google ID

    <not supported>

    Google Workspace

    Configuring Single Sign-On

    Providers Configuration

    Google Workspace

    Google Admin control panel

    Appmixer

    depends on configuration

    Accounts

    Authentication to apps.

    Get Accounts

    GET [TENANT_API_URL]/auth/:componentType

    Get the list of accounts the user has previously authenticated with for this component type. curl "https://api.acme.com/auth/appmixer.slack.list.SendChannelMessage?componentId=e15ef119-8fcb-459b-aaae-2a3f9ee41f15" -H "Authorization: Bearer [ACCESS_TOKEN]"

    Path Parameters

    Name
    Type
    Description
    Name
    Type
    Description

    GET [TENANT_API_URL]/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]"

    Name
    Type
    Description

    Example of filtering certain accounts:

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

    Name
    Type
    Description
    Name
    Type
    Description

    POST [TENANT_API_URL]/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).

    Name
    Type
    Description
    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:

    POST [TENANT_API_URL]/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]"

    Name
    Type
    Description

    DELETE [TENANT_API_URL]/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]"

    Name
    Type
    Description

    GET [TENANT_API_URL]/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]"

    Name
    Type
    Description

    POST [TENANT_API_URL]/auth/ticket

    Generate an authentication session ticket. This is the first call to be made before the user can authentication to a service. The flow is as follows: 1. Generate an authentication session ticket. 2. Get an authentication URL. 3. Start an authentication session. 4. Open the authentication URL in a browser to start the authentication flow. 5. Once the user completes the authentication flow, the browser redirects the user to a special Appmixer page which posts a message of the form "appmixer.auth.[success/failure].[ticket]" via the window.postMessage() call: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage. Note that this is a low-level mechanism that you don't have to normally deal with. The Appmixer JS SDK handles all this for you. curl "https://api.appmixer.com/auth/ticket" -H "Authorization: Bearer [ACCESS_TOKEN]"

    GET [TENANT_API_URL]/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]"

    Name
    Type
    Description
    Name
    Type
    Description

    GET [TENANT_API_URL]/auth/status/:ticket

    DELETE [TENANT_API_URL]/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]"

    Name
    Type
    Description

    PUT [TENANT_API_URL]/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]"

    Name
    Type
    Description

    POST http://[API-URL]/accounts/:accountId/share

    Share your account with an integration template so that instances created from the template can use your account. This is useful when you want template users to access shared resources (like a database or API) without requiring them to provide their own credentials.

    Name
    Type
    Description
    Name
    Type
    Description

    The account doesn't have valid tokens with the required scope for the component type.

    You don't own this account and cannot share it.

    The specified flow (integration template) doesn't exist.

    Shared Database Access

    API Rate Limit Management

    POST http://[API-URL]/accounts/:accountId/unshare

    Remove account sharing from an integration template. This immediately revokes access for all instances created from the template.

    Name
    Type
    Description
    Name
    Type
    Description

    You don't own this account and cannot unshare it.

    The specified flow doesn't exist.

    service

    string

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

    token

    object

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

    profileInfo

    object

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

    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

    displayName

    string

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

    name

    string

    accountId

    string

    Account ID.

    accountId

    string

    Account ID.

    accountId

    string

    Account ID.

    ticket

    string

    Authentication ticket.

    componentType

    string

    string

    Component ID.

    componentId

    string

    Component ID.

    accountId

    string

    Account ID.

    componentId

    string

    accountId

    string

    ID of the account to share

    flowId*

    string

    ID of the integration template to share the account with

    componentIds*

    array

    accountId

    string

    ID of the account to unshare

    flowId*

    string

    ID of the integration template to unshare from

    Query Parameters

    Get All Accounts

    Query Parameters

    Update Account Info

    Path Parameters

    Request Body

    Create Account

    Query Parameters

    Request Body

    Test Account

    Path Parameters

    Remove Account

    Path Parameters

    List All Flows Using Account

    Path Parameters

    Generate Authentication Session Ticket

    Get Authentication URL

    Path Parameters

    Query Parameters

    Get Authentication Status

    Clear Authentication From Component

    Path Parameters

    Assign Account To Component

    Path Parameters

    Share Account

    Path Parameters

    Request Body

    Component-Level Sharing

    Account sharing is done at the component level, not the template level. This allows you to share your account for specific components while requiring users to connect their own accounts for other components in the same template.

    For example, in a template that syncs data between your S3 bucket and the user's S3 bucket, you can share your account for the trigger component while users provide their own account for the action component.

    Security & Scope Validation

    • Shared accounts are locked to specific component types for security

    • Components can only access tokens with the required scopes

    • Shared account credentials are automatically masked in logs for non-owners

    • Users cannot change component types to access unauthorized data

    Use Cases

    Unshare Account

    Path Parameters

    Request Body

    Immediate Effect

    Unsharing an account immediately affects all template instances. Components using the shared account will lose access and flows may stop working. Ensure users are notified before unsharing critical accounts.

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

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

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

    Component type.

    Component ID.

    Array of component IDs that should use this shared account

    [
      {
        "accountId": "5a6e21f3b266224186ac7d03",
        "name": "U0UFJ0MFG - client IO",
        "displayName": null,
        "service": "appmixer:slack",
        "userId": "58593f07c3ee4f239dc69ff7",
        "profileInfo": {
          "id": "U0UFJ0MFG - client IO"
        },
        "icon": "data:image/png;base64,...rkJggg==",
        "label": "Slack"
      },
      {
        "accountId": "5a7313abb3a60729efe76f1e",
        "name": "t.o.mas@client.io",
        "displayName": null,
        "service": "appmixer:pipedrive",
        "userId": "58593f07c3ee4f239dc69ff7",
        "profileInfo": {
          "name": "tomas",
          "email": "t.o.mas@client.io"
        },
        "icon": "data:image/png;base64,...rkJggg==",
        "label": "Pipedrive"
      }
    ]  
    // filtering acme accounts and aws accounts
    curl --request GET 'http://api.acme.com/accounts?filter=service:!acme:[service]&filter=service:!appmixer:aws' \
    --header 'Authorization: Bearer [ACCESS_TOKEN]'
    {
        "accountId": "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"
    }
    curl -XPOST "http://[API-URL]/accounts/5a6e21f3b266224186ac7d03/share" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "flowId": "integration-template-id",
        "componentIds": ["component-1", "component-2"]
      }'
    {
      "accountId": "5a6e21f3b266224186ac7d03",
      "flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
      "shared": [
        {
          "componentId": "component-1",
          "componentType": "appmixer.gmail.SendEmail"
        },
        {
          "componentId": "component-2",
          "componentType": "appmixer.slack.SendMessage"
        }
      ]
    }
    {
      "statusCode": 400,
      "error": "Bad Request",
      "message": "No valid tokens found for componentType: appmixer.gmail.SendEmail"
    }
    {
      "statusCode": 403,
      "error": "Forbidden",
      "message": "Insufficient permissions"
    }
    {
      "statusCode": 404,
      "error": "Not Found",
      "message": "Flow 9089f275-f5a5-4796-ba23-365412c5666e not found"
    }
    # Template creator connects to their database
    # End users don't need database credentials
    curl -XPOST "http://[API-URL]/accounts/db-account-id/share" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "flowId": "data-sync-template",
        "componentIds": ["postgres-query-component"]
      }'
    # Share premium API account with higher rate limits
    # Users don't consume their own API quotas
    curl -XPOST "http://[API-URL]/accounts/premium-api-account/share" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "flowId": "analytics-template",
        "componentIds": ["api-fetch-component"]
      }'
    curl -XPOST "http://[API-URL]/accounts/5a6e21f3b266224186ac7d03/unshare" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "flowId": "integration-template-id"
      }'
    {
      "accountId": "5a6e21f3b266224186ac7d03",
      "flowId": "9089f275-f5a5-4796-ba23-365412c5666e"
    }
    {
      "statusCode": 403,
      "error": "Forbidden",
      "message": "Insufficient permissions"
    }
    {
      "statusCode": 404,
      "error": "Not Found",
      "message": "Flow 9089f275-f5a5-4796-ba23-365412c5666e not found"
    }
    /:/.well-known
    1,5,60,300,720
    validation errors
    HTTP cookies RFC specification
    Configuring Single-Sing-On
    Configuring Single-Sing-On
    Configuring Single-Sing-On
    Public files

    Designer

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

    The Designer renders flows with a redesigned diagram engine. The new diagram uses the Modern theme by default, in both light and dark modes, and is the production default wherever the standard left-to-right layout is in use.

    • The layout comes from the theme, not from individual flows: when the themed action and trigger carry orientation: 'bottom' — the vertical shape presets — the Designer falls back to the previous diagram for every flow. Detection is keyed on that attribute rather than on the shape name, so custom shapes registered with appmixer.registerComponentShape

    participate too. The top-to-bottom layout is kept as a backup only and is scheduled for removal in a future release.
  • The layout setting travels inside the theme, and changing the theme unmounts and remounts the Designer, so the orientation is resolved once per Designer lifetime.

  • The new diagram supports one-click re-layout of the flow — see the align toolbar button below.

  • Zooming with the pointer follows the platform convention: ⌘ + scroll on macOS and Ctrl + scroll on Windows and Linux zoom about the pointer. On macOS, ⌘ + scroll previously panned the canvas; on Windows and Linux, Super + scroll still pans.

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

    Type: String | Default: null

    ID of a flow that is opened in the editor.

    Type: String | Default: null

    ID of a component that is opened in the editor.

    Type: Object | Default: DefaultShareTypes

    Override default sharing dialog types.

    Type: Object[] | Default: DefaultSharePermissions

    Override default sharing dialog permissions.

    Type: Boolean | Default: true

    Toggle visibility of the header. &#xNAN;config.options.validation

    Type: Object | Default: {}

    Controls validation panel settings. Properties:

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

    Example:

    Type: Object[] | Default: []

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

    Type: Array[] | Default: []

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

    'align'

    The align button key adds an Align Diagram button that re-arranges the whole flow into a clean, layered left-to-right layout with a single click. Branches are ordered by the output port their links leave from, notes are never moved, and the re-layout finishes with a zoom-to-fit. The alignment only changes component positions — links, ports and the flow logic are untouched — and it is persisted as a single flow update, so it can be undone in one step.

    'commands'

    The commands button key adds a View Commands button that opens the Keyboard Commands panel — a floating panel beside the canvas listing every keyboard and pointer shortcut the Designer supports, from copy/paste and undo/redo to marquee selection, nudging and note editing. Each row shows the macOS and the Windows/Linux binding, and the row highlights as you press its shortcut. Keystrokes aimed at text fields are ignored, so typing in an inspector field never flashes a row.

    'logs'

    The logs button key adds the View Logs button, and integration-test-logs adds View Test Logs for integration tests. Together they decide whether the Designer's built-in logs panel is available at all — they are not only about the buttons.

    Type: Boolean | Default: true

    Automatically open logs view when the flow is running. It applies only while the toolbar offers a logs button — with the logs keys removed from toolbar, the built-in panel is switched off and nothing opens it (see the 'logs' key under config.options.toolbar above).

    The logs panel refreshes automatically while its tab is active. Polling is suspended while the browser tab is hidden and resumes when the tab becomes active again; if the API rate-limits the requests, the panel backs off and recovers on its own.

    Type: Object | Default: null

    Configures how a flow with no trigger invites the user to add one. When enabled is true, an empty canvas shows a placeholder with an Add a trigger tile and, where the AI Copilot is available, a Build with AI tile. Choosing Add a trigger opens the trigger selector dialog, whose top entries can be curated with featured.

    Property
    Type
    Default
    Description

    enabled

    Boolean

    false

    Turns the trigger selector on.

    Type: Object | Default: null

    Configuration of the AI Copilot panel. The Copilot button is shown only when the Copilot plugin is enabled on the backend.

    Type: Boolean | Default: null

    Toggle a custom loading state.

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

    copilotSessions

    Type: Object | Default: {}

    Pointers to the AI Copilot conversations the user has started, as a { [contextKey]: threadId } map — contextKey is 'flow:<flowId>' for a conversation bound to a flow.

    Treat the map as opaque: read it, store it, hand it back. Its purpose is to let a Copilot conversation survive a page reload — persist it (for example to sessionStorage) when the Designer fires the change event, and pass it back in state the next time you create the Designer. Reopening the same flow then resumes its conversation instead of starting a new one.

    It holds the conversation context only, never the messages: the backend owns the transcript and the panel restores it from the threadId when the conversation is reopened.

    Opens the AI Copilot panel and submits message into the conversation the user can see, exactly as if they had typed it in the composer.

    Use it to drive Copilot from your own UI — for example a Fix it with Copilot button in a dialog that explains what a flow created from a template still needs from the user.

    Everything else about the prompt is unchanged: the reply streams into the panel, copilot:chat-start fires for a new conversation, the thread is retained in copilotSessions, and the confirmation the composer shows before creating or replacing an automation draft is shown for an SDK prompt too.

    The promise resolves once the prompt has been submitted — not when Copilot has finished answering.

    Calling it immediately after designer.set('flowId', id) is safe: the prompt waits for the flow to finish loading before it is sent. A flow that cannot be loaded at all — a wrong or inaccessible id — is reported as no-flow, which can take up to 30 seconds.

    Property
    Type
    Description

    sent

    Boolean

    Whether the prompt was submitted.

    reason

    String

    reason

    Meaning

    unavailable

    The Copilot plugin is not enabled on the backend, or this Designer runs in integration template mode.

    not-mounted

    The Designer is not open — designer.open() has not been called, designer.close() has, or it was closed while the prompt was waiting.

    no-flow

    A refused prompt leaves the Designer as it was — with one deliberate exception: declined means the user was asked something, so the panel is open, showing the question they answered.

    Toggle stage button to start the flow.

    Toggle stage button to stop the flow.

    Click menu item to open sharing of the flow.

    Click menu item to rename the flow.

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

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

    Click menu item to print diagram of the flow.

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

    Click menu item to open a wizard builder dialog.

    Add a new component to the flow.

    Open component inspector.

    Close component inspector.

    Rename a component.

    Use selection input to change component type.

    Click a button to show validation errors.

    An AI Copilot conversation has begun. Fires once per conversation, no matter how it was started — the composer, a suggestion chip, or designer.copilot.send().

    Property
    Type
    Description

    threadId

    String

    The conversation assigned by the backend, which owns its message history.

    flowId

    String | null

    Resuming a conversation does not fire this event — nothing started. The Designer keeps the pointer in copilotSessions for you, so listen to this only if you want the threadId for your own purposes.

    Diagram

    shapes
    Designer
    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()
                      }
                  }
              }
            }]
        ]
      }
    })
    appmixer.ui.Designer({
      /* ... */
      options: {
          toolbar: [
            ['undo', 'redo'],
            ['zoom-to-fit', 'zoom-in', 'zoom-out'],
            ['align'],
            ['logs']
        ]
      }
    })
    appmixer.ui.Designer({
      /* ... */
      options: {
          toolbar: [
            ['undo', 'redo'],
            ['zoom-to-fit', 'zoom-in', 'zoom-out'],
            ['align', 'commands'],
            ['logs']
        ]
      }
    })
    appmixer.ui.Designer({
      /* ... */
      options: {
          toolbar: [
            ['undo', 'redo'],
            ['zoom-to-fit', 'zoom-in', 'zoom-out'],
            ['logs']
        ]
      }
    })
    const designer = appmixer.ui.Designer({
      /* ... */
      options: {
          autoOpenLogs: true
          toolbar: [
            ['logs']
        ]
      }
    })
    const designer = appmixer.ui.Designer({
      /* ... */
      options: {
        triggerSelector: {
          enabled: true,
          autoOpen: false,
          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,...',
            }
          ]
        }
      }
    })
    const designer = appmixer.ui.Designer({
      /* ... */
      options: {
        copilot: {
          // Show the built-in, localized example prompts:
          examplePrompts: true
          // — or — provide your own (replaces the defaults):
          // examplePrompts: [
          //   'When a Stripe payment succeeds, add a row to Google Sheets'
          // ]
          // Omit or set false to keep them hidden (default).
        }
      }
    })
    designer.state(name, value)
    const designer = appmixer.ui.Designer({
      el: '#designer',
      state: {
        copilotSessions: JSON.parse(sessionStorage.getItem('copilotSessions') || '{}')
      }
    })
    
    designer.on('change', state => {
      sessionStorage.setItem('copilotSessions', JSON.stringify(state.copilotSessions))
    })
    const { sent, reason } = await designer.copilot.send(message)
    const designer = appmixer.ui.Designer({ el: '#designer' })
    designer.open()
    designer.set('flowId', flowId)
    
    // Your own "Fix it with Copilot" button:
    const { sent, reason } = await designer.copilot.send(
      'This flow was created from a template. Help me fill in the product fields.'
    )
    
    if (!sent && reason === 'busy') {
      showHint('Copilot is still working on the previous request.')
    }
    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) => {/* ... */})
    designer.on('copilot:chat-start', ({ data }) => {/* ... */})
    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()

    Configuration

    config.el ...

    Learn about widget config .

    config.flowId

    config.componentId

    config.shareTypes

    config.sharePermissions

    config.options.showHeader

    Additional validation options may be added in future versions.

    config.options.menu

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

    config.options.toolbar

    Specify Vue under widget to create a custom toolbar button.

    The button is available only with the new diagram (left-to-right layouts) and is disabled when the Designer is read-only. Removing the 'align' string from the toolbar array fully disables the feature.

    The button sits between Zoom Out and Logs and is available only with the new diagram. Removing the 'commands' string from the toolbar array fully disables it. The panel is themable under the '#Designer/#commandsPanel' scope.

    With neither key in the toolbar array, the whole panel is switched off: the View Logs button and the chevron toggle on the right of the toolbar are both gone, a running flow no longer opens logs by itself regardless of the autoOpenLogs option below, and a test run's View logs links — on the result badge and in a component's test-output popover — are not offered. Use this when your application shows flow logs in its own UI.

    config.options.autoOpenLogs

    config.options.triggerSelector

    The placeholder requires a genuinely empty canvas — no components and no notes — so the centred tiles never cover existing content. The Build with AI tile additionally requires the Copilot system plugin to be enabled on the backend and a surface that allows Copilot; it is hidden in integration template mode.

    config.options.copilot

    The Designer also includes a Test flow button that executes a single test run of the flow without starting it (streaming per-component results onto the canvas, with dedicated test-input dialogs for Webhook, Form and Chat triggers), a Versioning panel with the flow's version history and draft management (see ), a floating validation panel that replaces the validation sidebar, and per-component Error handling settings (see ).

    Instance

    Learn about widget instance .

    State

    loader

    error

    Methods

    designer.copilot.send

    Available since Appmixer 6.5.5.

    Sending a message straight to the Copilot API instead reaches the same server-side conversation, but it will not appear in an open panel — the panel renders its own transcript. Use this method whenever the user should see the prompt.

    Events

    flow:start

    flow:stop

    flow:share

    flow:rename

    flow:export-svg

    flow:export-png

    flow:print

    flow:validation

    flow:wizard-builder

    component:add

    component:open

    component:close

    component:rename

    component:update-type

    navigate:validation

    copilot:chat-start

    Example

    Endpoint /logs

    Endpoint logs return log messages with various data structures.

    In this section, we will examine five types of response logs, including their common parameters and other details.

    GET or POST [TENANT_API_URL]/logs

    Complete documentation for the appmixer application API protocols can be found at:

    Common Core

    These fields are present in all observed variants.

    Field
    Type
    Format / Notes

    Observed as the regular component-level data event, typically outbound, with id = "component" and type = "data". In the sample, portType is out for this variant.

    Field
    Type
    Format / Notes

    Observed as the inbound queue-style data event, with id = "input-queue" and type = "data". In the sample, portType is in for this variant.

    Field
    Type
    Format / Notes

    Observed as the flow-level event with id = "flow" and type = "flow".

    Field
    Type
    Format / Notes

    Observed as a component-related error with id = "component", severity = "error", and without type in the sampled minimal form.

    Field
    Type
    Format / Notes

    Observed as the system-level retry error with id = "retry" and indexPrefix = "system".

    Field
    Type
    Format / Notes

    ```json { "severity": "error", "componentId": "a4833f9f-0c29-4911-bff2-c4f73b0f1b13", "indexPrefix": "system", "tenantId": "dev-automated-00001", "es_index": "dev-automated-00001-appmixer-202601", "id": "retry", "gridTimestamp": "2026-01-21T14:28:01.715Z", "flowId": "49dfa93c-eda8-4d91-a345-b5f2ebdbae67", "timestamp": "2026-01-21T14:28:01.768048338Z", "_id": "vfD04JsBdjlz-szdjt4D", "_index": "dev-automated-00001-appmixer-202601" } ```

    These are the constant-like fields that help identify the output variant:

    Variant
    id
    indexPrefix
    severity
    type
    portType

    string

    UUID-like string

    gridTimestamp

    string

    ISO 8601 UTC datetime

    id

    string

    Constant-like discriminator, observed values include component, input-queue, flow, retry

    indexPrefix

    string

    Constant-like discriminator, observed values include logstash, system

    severity

    string

    Constant-like discriminator, observed values include info, error

    tenantId

    string

    Tenant identifier

    timestamp

    string

    ISO 8601 UTC datetime

    Elasticsearch index name

    componentId

    string

    UUID-like string

    componentType

    string

    Component type identifier

    correlationId

    string

    UUID-like string

    testRunId

    string

    Present only on records produced by a ; its presence marks the record as a test execution.

    staticCall

    boolean

    Present only on records produced by a with log: true (AI Agent tools observability).

    es_index

    string

    Source index name

    flowId

    string

    UUID-like string

    flowName

    string

    Flow name

    flowType

    string

    Observed constant value: automation

    gridTimestamp

    string

    ISO 8601 UTC datetime

    id

    string

    Observed constant value: component

    indexPrefix

    string

    Observed constant value: logstash

    level

    integer

    Optional in sample, present in newer-looking records

    messageId

    string

    UUID-like string, optional in sample

    port

    string

    Port name, observed values include request, out, item, done

    portType

    string

    Observed constant value in this variant: out

    senderId

    string

    UUID-like string

    senderType

    string

    Sender component type identifier

    severity

    string

    Observed constant value in this variant: info

    tenantId

    string

    Tenant identifier

    timestamp

    string

    ISO 8601 UTC datetime

    type

    string

    Observed constant value: data

    userId

    string

    User identifier

    Elasticsearch index name

    bundleId

    string

    UUID-like string

    componentId

    string

    UUID-like string

    componentType

    string

    Component type identifier

    correlationId

    string

    UUID-like string

    es_index

    string

    Source index name

    flowId

    string

    UUID-like string

    flowName

    string

    Flow name

    flowType

    string

    Observed constant value: automation

    gridTimestamp

    string

    ISO 8601 UTC datetime

    id

    string

    Observed constant value: input-queue

    indexPrefix

    string

    Observed constant value: logstash

    level

    integer

    Optional in sample

    messageId

    string

    UUID-like string, optional in sample

    port

    string

    Observed constant value in this variant: in

    portType

    string

    Observed constant value in this variant: in

    senderId

    string

    UUID-like string

    senderPort

    string

    Port name, observed values include out, item

    senderType

    string

    Sender component type identifier

    severity

    string

    Observed constant value in this variant: info

    tenantId

    string

    Tenant identifier

    testRunId

    string

    Present only on records produced by a ; its presence marks the record as a test execution.

    timestamp

    string

    ISO 8601 UTC datetime

    type

    string

    Observed constant value: data

    userId

    string

    User identifier

    Elasticsearch index name

    dataMessageType

    string

    Observed constant value in sample: componentLog

    es_index

    string

    Source index name

    flowId

    string

    UUID-like string

    flowName

    string

    Flow name

    flowType

    string

    Observed constant value: automation

    gridTimestamp

    string

    ISO 8601 UTC datetime

    id

    string

    Observed constant value: flow

    indexPrefix

    string

    Observed constant value: logstash

    level

    integer

    Optional in sample

    severity

    string

    Observed constant value in sample: info

    tenantId

    string

    Tenant identifier

    timestamp

    string

    ISO 8601 UTC datetime

    type

    string

    Observed constant value: flow

    userId

    string

    User identifier

    Elasticsearch index name

    componentId

    string

    UUID-like string

    componentType

    string

    Component type identifier

    correlationId

    string

    UUID-like string

    es_index

    string

    Source index name

    flowId

    string

    UUID-like string

    gridTimestamp

    string

    ISO 8601 UTC datetime

    id

    string

    Observed constant value: component

    indexPrefix

    string

    Observed constant value: logstash

    senderId

    string

    UUID-like string

    senderType

    string

    Sender component type identifier

    severity

    string

    Observed constant value: error

    tenantId

    string

    Tenant identifier

    testRunId

    string

    Present only on records produced by a ; its presence marks the record as a test execution.

    timestamp

    string

    ISO 8601 UTC datetime

    userId

    string

    User identifier

    componentId

    string

    UUID-like string

    es_index

    string

    Source index name

    flowId

    string

    UUID-like string

    gridTimestamp

    string

    ISO 8601 UTC datetime

    id

    string

    Observed constant value: retry

    indexPrefix

    string

    Observed constant value: system

    severity

    string

    Observed constant value: error

    tenantId

    string

    Tenant identifier

    timestamp

    string

    ISO 8601 UTC datetime

    data

    out

    Input queue data event

    input-queue

    logstash

    info

    data

    in

    Flow event

    flow

    logstash

    info

    flow

    —

    Component error event

    component

    logstash

    error

    —

    —

    System retry error event

    retry

    system

    error

    —

    —

    _id

    string

    Elasticsearch document id

    _index

    string

    Elasticsearch index name

    es_index

    string

    Source index name

    {
      "_id": "38j04JsB5N8vaDg6yxaZ",
      "_index": "dev-automated-00001-module-202601",
      "es_index": "dev-automated-00001-module-202601",
      "flowId": "49dfa93c-eda8-4d91-a345-b5f2ebdbae67",
      "gridTimestamp": "2026-01-21T14:28:17.138Z",
      "id": "component",
      "indexPrefix": "logstash",
      "severity": "info",
      "tenantId": "dev-automated-00001",
      "timestamp": "2026-01-21T14:28:17.146174532Z"
    }

    _id

    string

    Elasticsearch document id

    _index

    {
      "severity": "info",
      "componentType": "appmixer.utils.controls.Each",
      "componentId": "201dd446-c259-4023-9118-5315539533ff",
      "level": 30,
      "messageId": "71b49651-7fb3-496a-815c-3186f65774fe",
      "type": "data",
      "flowName": "Test each",
      "userId": "695312affbdyyyb75e0cc360",
      "portType": "out",
      "senderId": "201dd446-c259-4023-9118-5315539533ff",
      "port": "item",
      "indexPrefix": "logstash",
      "tenantId": "dev-automated-00001",
      "correlationId": "545a95da-8a69-4103-b8d1-bebc9ec909d8",
      "senderType": "appmixer.utils.controls.Each",
      "es_index": "dev-automated-00001-module-202603",
      "id": "component",
      "gridTimestamp": "2026-03-17T16:29:11.910Z",
      "flowId": "49dfa93c-eda8-4d91-a345-b5f2ebdbae67",
      "flowType": "automation",
      "timestamp": "2026-03-17T16:29:11.918173099Z",
      "_id": "Ioyh_JwBw7IAskcYQtwv",
      "_index": "dev-automated-00001-module-202603"
    }

    _id

    string

    Elasticsearch document id

    _index

    {
      "severity": "info",
      "componentType": "appmixer.utils.controls.SetVariable",
      "senderPort": "item",
      "componentId": "67d06917-95fb-44f4-b7aa-f2ea6196ed32",
      "level": 30,
      "bundleId": "022adc03-2719-45e1-a18a-818e000f2d5f",
      "messageId": "5ba614d8-386a-4367-aa70-4753dafb8682",
      "type": "data",
      "flowName": "Test each",
      "userId": "694699affbdxxxb75e0cc360",
      "portType": "in",
      "senderId": "201dd446-c259-4023-9118-5315539533ff",
      "port": "in",
      "indexPrefix": "logstash",
      "tenantId": "dev-automated-00001",
      "correlationId": "545a95da-8a69-4103-b8d1-bebc9ec909d8",
      "senderType": "appmixer.utils.controls.Each",
      "es_index": "dev-automated-00001-module-202603",
      "id": "input-queue",
      "gridTimestamp": "2026-03-17T16:29:11.252Z",
      "flowId": "49dfa93c-eda8-4d91-a345-b5f2ebdbae67",
      "flowType": "automation",
      "timestamp": "2026-03-17T16:29:11.317881731Z",
      "_id": "PYqh_JwBbUptlPbkPuoX",
      "_index": "dev-automated-00001-module-202603"
    }

    _id

    string

    Elasticsearch document id

    _index

    {
      "severity": "info",
      "dataMessageType": "componentLog",
      "type": "flow",
      "flowName": "Test each",
      "userId": "695399affbdxxxb75e0cc385",
      "indexPrefix": "logstash",
      "tenantId": "dev-automated-00001",
      "es_index": "dev-automated-00001-module-202603",
      "id": "flow",
      "gridTimestamp": "2026-03-17T16:29:11.252Z",
      "flowId": "49dfa93c-eda8-4d91-a345-b5f2ebdbae67",
      "flowType": "automation",
      "timestamp": "2026-03-17T16:29:11.260000000Z",
      "_id": "exampleFlowEventId",
      "_index": "dev-automated-00001-module-202603"
    }

    _id

    string

    Elasticsearch document id

    _index

    {
      "severity": "error",
      "componentType": "appmixer.utils.controls.Each",
      "componentId": "a4833f9f-0c29-4911-bff2-c4f73b0f1b13",
      "userId": "695312affbdxxxb75e0cc370",
      "senderId": "6fbd1b6a-10c3-4b8b-ac8d-68985f1fb8af",
      "indexPrefix": "logstash",
      "tenantId": "dev-automated-00001",
      "correlationId": "7fe84058-ad1a-4a6d-8a59-d015a9d7c4f5",
      "senderType": "appmixer.utils.http.WebhookTrigger",
      "es_index": "dev-automated-00001-module-202601",
      "id": "component",
      "gridTimestamp": "2026-01-21T14:28:01.714Z",
      "flowId": "49dfa93c-eda8-4d91-a345-b5f2ebdbae67",
      "timestamp": "2026-01-21T14:28:01.765321479Z",
      "_id": "uvD04JsBdjlz-szdjt4D",
      "_index": "dev-automated-00001-module-202601"
    }

    _id

    string

    Elasticsearch document id

    _index

    string

    Component data event

    component

    logstash

    Variant 1: Component data event

    Variant 2: Input queue data event

    Variant 3: Flow event

    Variant 4: Component error event

    Variant 5: System retry error event

    Minimal summary of observed discriminators

    flowId

    string

    string

    string

    string

    Elasticsearch index name

    info

    autoOpen

    Boolean

    false

    Restores the legacy behaviour — the trigger selector dialog opens by itself on a flow with no trigger. Mutually exclusive with the placeholder: when autoOpen is true, the placeholder is not shown.

    featured

    Array

    —

    Components pinned to the top of the dialog, optionally with a custom label, description, marker and icon.

    Why it was not. Present only when sent is false.

    No flow is open, or the flow could not be loaded.

    busy

    A Copilot request or a test run is already working on this flow, or an earlier send() is still being processed. Worth retrying when it finishes.

    declined

    The prompt reached Copilot and was not sent: the user dismissed the draft confirmation, or it was superseded first — the flow was switched while the confirmation was on screen, or another request claimed Copilot.

    empty-message

    The message was empty or whitespace only.

    error

    The Designer failed on its way to sending; the reason is logged to the browser console.

    The flow the conversation was started for. null for a conversation not bound to a flow.

    context

    Object | null

    The same binding, structured: { type: 'flow', flowId }. null when no flow is open.

    here
    ComponentOptions
    Flow Versions
    Handle Flow Errors
    here

    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.

    properties.schema

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

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

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

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

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

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

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

    A single line input field.

    A multi-line text input field.

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

    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.

    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.

    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.

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

    Option
    Description

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

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

    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.

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

    The value of this field has the following structure:

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

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

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

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

    3. When opening the variables picker in myText

    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:

    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.

    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.

    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.

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

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

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

      • eq: Equality between the values.

      • equal:

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

    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:

    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:

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

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

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

    Example:

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

    flow test run
    static component call
    flow test run
    flow test run

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

    maxDate

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

    minDate

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

    mode

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

    time_24hr

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

    weekNumbers

    Boolean. Enables display of week numbers in calendar.

    field inside the second group, only
    variableB
    will be available, because
    variableA
    is already been used.
    Equality between the values by deep comparison. Used for objects and arrays.
  • ne: Values are not equal.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    format

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

    enableTime

    Boolean. Enables time picker.

    enableSeconds

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

    properties.inspector

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

    Inspector built-in types:

    text

    textarea

    number

    key-value

    select

    multiselect

    date-time

    toggle

    color-palette

    select-button-group

    expression

    filepicker

    googlepicker

    onedrivepicker

    Conditional fields

    properties.source

    properties.source.url

    properties.source.data.messages

    properties.source.data.properties

    properties.source.data.transform

    Rappid Inspector definition format
    CSS color formats
    source
    Invalid Inspector field
    Configuration Overview
    source with expand

    Boolean. Enables seconds in the time picker.

    Behaviour

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

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

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

    Component Virtual Methods

    As was mentioned in the previous paragraph, components are simple NodeJS modules that can implement a certain set of methods the Appmixer engine understands. The one most important method is the receive() method. This method is called by the engine every time messages are available on the input ports and the component is ready to execute. The method must return a promise that when resolved, acknowledges the processing of the input messages. If the promise is rejected, the Appmixer engine automatically retries to send the messages later using an exponential back-off strategy that prolongs intervals between the retries.

    Messages that have been rejected 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().

    Virtual Method
    Description

    Flows can be executed in a test mode without being started (see ). When a test run starts from a trigger and no explicit test data is supplied, the engine resolves the trigger's output using a fallback chain:

    1. test(context) — if the trigger implements the test() virtual method, it is called to produce real sample data. Use the same context.sendJson() API as in receive()/tick().

    2. Flow logs — otherwise, the engine looks for output data from past runs of the component in the flow logs.

    Note that in test mode context.stopFlow() and context.sendSignal() are no-ops, component/flow state is ephemeral (never touches production state), and start()/stop() are not called for the tested trigger.

    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.

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

    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.

    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.

    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:

    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.

    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:

    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.

    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.

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

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

    Get a state value stored under key.

    Remove a value under key.

    Clears the entire state.

    Add value into set under key.

    Remove value from set under key.

    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.

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

    Load the state from the DB.

    Set a state key to hold the value. key must be a string. value can be a string, number or a JSON object.

    Get a state value stored under key.

    Remove a value under key.

    Clears the entire state.

    Add value into a Set stored under key.

    Remove value from Set stored under key.

    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.

    This is similar to the component state, but the service state is available across all components in the connector.

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

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

    Get a state value stored under key.

    Remove a value under key.

    Clears the entire state.

    Add value into a Set stored under key.

    Remove value from Set stored under key.

    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.

    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.

    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.

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

    Return object:

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

    Example return object:

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

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

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

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

    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:

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

    Property
    Description

    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.

    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.

    Get the list of user's Data Stores.

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

    Set value to the Data Store under the key.

    Remove the key from the Data Store.

    Clear all data from the Data Store.

    Find items in the Data Store.

    Get a cursor.

    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.

    Unregister a webhook.

    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.

    Clear (cancel) a scheduled timeout.

    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:

    Stop the running flow. Example:

    The ID of the component.

    The ID of the flow the component runs in.

    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.

    Flow properties are available in this object.

    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.

    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.

    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:

    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:

    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, number, 30 by default

    Example:

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

    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.

    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.

    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.

    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.

    Flows

    GET [TENANT_API_URL]/flows

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

    Name
    Type
    Description

    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.

    test(context)

    Called when the user runs a flow test run ( or the Test flow button in the Designer). Implement it in trigger components to fetch a real sample item (e.g. the most recent record from the 3rd party API) and send it to the output port.

    Schema sample — otherwise, sample data is generated from the output port's JSON Schema (the schema's example values are used when present, see Example values).

    content.query

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

    content.data

    Object with the body parameters of the request.

    correlationId

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

    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)

    module.exports = {
    
        async test(context) {
            // Fetch the most recent item so the user tests with real data.
            const { data } = await context.httpRequest({
                method: 'GET',
                url: 'https://api.example.com/items?limit=1',
                headers: { Authorization: `Bearer ${context.auth.accessToken}` }
            });
            return context.sendJson(data.items[0], 'out');
        }
    };
    {
        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();
        }
    };
    

    content.method

    HTTP method of the request.

    content.hostname

    Hostname of the Appmixer API.

    content.headers

    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: 'john@example.com',
            approver: 'alice@example.com',
            decisionBy: (function() {
                const tomorrow = new Date;
                tomorrow.setDate(tomorrow.getDate() + 1);
                return tomorrow.toISOString();
            })()
        }
    });
    module.exports = {
        async receive(context) {
            return context.stopFlow();
        }
    };
    '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;
            }
        }
    };
    

    Providing test data with test(context)

    Context

    Input/Output message(s)

    context.messages

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

    async context.sendJson(messageContent, outPort)

    async context.sendArray(arrayOfObjects, outPort)

    Authentication

    context.auth

    Backoffice configuration

    context.config

    Properties

    context.properties

    Component State

    context.state

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

    async context.loadState()

    async context.saveState(object)

    async context.stateSet(key, value)

    async context.stateGet(key)

    async context.stateUnset(key)

    async context.stateClear()

    async context.stateAddToSet(key, value)

    async context.stateRemoveFromSet(key, value)

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

    Flow State

    async context.flow.loadState()

    async context.flow.stateSet(key, value)

    async context.flow.stateGet(key)

    async context.flow.stateUnset(key)

    async context.flow.stateClear()

    async context.flow.stateAddToSet(key, value)

    async context.flow.stateRemoveFromSet(key, value)

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

    Service State

    async context.service.loadState()

    async context.service.stateSet(key, value)

    async context.service.stateGet(key)

    async context.service.stateUnset(key)

    async context.service.stateClear()

    async context.service.stateAddToSet(key, value)

    async context.service.stateRemoveFromSet(key, value)

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

    Files

    async context.saveFile(fileName, mimeType, buffer)

    async context.saveFileStream(fileName, stream)

    async context.replaceFileStream(fileId, stream)

    async context.getFileInfo(fileId)

    async context.loadFile(fileId)

    context.readFileStream(fileId)

    async context.getFileReadStream(fileId)

    async context.removeFile(fileId)

    Webhook

    context.getWebhookUrl()

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

    async context.response(body, statusCode, headers)

    HTTP

    async context.httpRequest

    Store

    async context.store.listStores()

    async context.store.get(storeId, key)

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

    async context.store.remove(storeId, key)

    async context.store.clear(storeId)

    async context.store.find(storeId, query)

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

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

    async context.store.unregisterWebhook(storeId);

    Scheduling

    async context.setTimeout(messageContent, delay)

    async context.clearTimeout(timeoutId)

    Miscellaneous

    async context.callAppmixer(request)

    async context.stopFlow()

    context.componentId

    context.flowId

    context.flowDescriptor

    context.customFields

    context.evalJavaScript(code, jsonData)

    context.setMaxWait(timestamp)

    async context.loadVariables()

    async context.log(object)

    async context.lock(lockName, options)

    Error Handling

    receive(context)

    tick(context)

    start(context)

    stop(context)

    Unprocessed Messages
    Test Flow
    context.saveFileStream
    AWS S3 GetFileObject
    context.getFileReadStream
    axios
    customFields
    quota
    unprocessedMessages
    Variables
    Authentication UI dialog for the above auth.js definition

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

    HTTP headers of the request.

    API section

    filter

    string

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

    sharedWithPermissions

    string

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

    projection

    string

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

    sort

    string

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

    pattern

    string

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

    offset

    number

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

    limit

    number

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

    [
      {
        "userId"
    

    GET [TENANT_API_URL]/flows/:id

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

    Name
    Type
    Description

    id

    string

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

    GET [TENANT_API_URL]/flows/count

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

    {
        "count": 29
    }    

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

    Name
    Type
    Description

    name

    string

    Name of the flow.

    customFields

    object

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

    Pass ?autoConnectAccounts=true to connect the user's available account to every component that needs one, after the flow has been saved. The same parameter works on Update Flow and Clone Flow.

    Value
    Behaviour

    true

    Assign an account only where exactly one is available.

    preferred

    With several accounts available, reuse an account already connected to another component of the same flow; otherwise take the first one deterministically.

    Account assignment runs after the flow is persisted and never fails the request — a flow whose components could not be given an account is still saved.

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

    Name
    Type
    Description

    id

    string

    Flow ID.

    Name
    Type
    Description

    forceUpdate

    boolean

    A running flow cannot be updated unless forceUpdate=true.

    Name
    Type
    Description

    object

    An object with flow, name, customFields, thumbnail, sharedWith, wizard, and notes parameters. flow is the Flow descriptor. See for the notes schema.

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

    DELETE [TENANT_API_URL]/flows/:id

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

    Name
    Type
    Description

    id

    string

    Flow ID.

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

    POST [TENANT_API_URL]/flows/:id/clone

    Clone a flow

    Name
    Type
    Description

    id*

    String

    Flow ID

    Name
    Type
    Description

    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

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

    Flow Drafts allow you to create editable versions of flows without affecting the original running flow. Drafts are primarily used for making changes to integration instances while the original continues to run.

    • Draft Types: automation-draft and integration-instance-draft

    • Workflow: Create draft → Make changes → Publish to update original flow

    • Safety: Only one draft per flow can exist at a time

    • Component ID Mapping: Drafts maintain a mapping to original component IDs for seamless publishing

    POST http://[API-URL]/flows/:id/clone

    Create a draft by cloning an existing flow with special draft parameters.

    Name
    Type
    Description

    id*

    String

    Flow ID to create draft from

    Name
    Type
    Description

    setOriginFlowId*

    Boolean

    Must be true to link draft to original flow

    setComponentIdMap*

    Boolean

    {
      "cloneId": "draft-flow-id"
    }
    {
      "cloneId": "existing-draft-flow-id"
    }

    Note: If a draft of the same type already exists for this flow, the existing draft ID is returned instead of creating a duplicate.

    POST http://[API-URL]/drafts/:draftFlowId/publish

    Publish a draft to update the original flow with the draft's changes.

    Name
    Type
    Description

    draftFlowId*

    String

    Draft flow ID to publish

    {
      "success": true,
      "flowId": "original-flow-id"
    }
    {
      "statusCode": 403,
      "error": "Forbidden",
      "message": "You do not have permission to publish this draft."
    }
    {
      "statusCode": 404,
      "error": "Not Found",
      "message": "Draft not found or invalid draft type."
    }

    When you publish a draft, the following operations occur automatically:

    1. Component ID Mapping: Draft component IDs are mapped back to the original flow's component IDs

    2. Flow Update: The original flow is updated with the draft's configuration

    3. Draft Deletion: The draft flow is automatically deleted after successful publishing

    4. Account Connections: If accounts were connected during draft creation, they are preserved

    Editing Integration Instances

    • Create a draft of a running integration instance

    • Make and test changes in the draft without affecting the live instance

    • Publish when ready to update the running instance

    Flow Versions keep a history of snapshots of a flow that you can list, restore, or clone into a new flow. Together with Flow Drafts, they power the version timeline in the Designer.

    • Version types: manual (created explicitly by the user), autosave (created automatically while editing), publish (created automatically when a draft is published) and restore-point (created automatically before a restore).

    • A version snapshot stores the flow descriptor together with the flow's name, customFields, wizard and notes.

    • Garbage collection: old autosave and restore-point versions are cleaned up automatically with configurable per-type retention and a total cap per flow.

    • Draft-aware restore: restoring a version of a running flow never modifies the live flow directly — instead, a draft is created (or replaced) with the version's content. Restoring on a stopped flow with no draft restores the flow in place.

    • Versions are deleted automatically when their flow (or the owning user) is deleted.

    POST [TENANT_API_URL]/flows/:id/versions

    Create a new version snapshot of the current state of the flow. Only the flow owner can create versions.

    curl -XPOST "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/versions" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{ "type": "manual", "label": "Before big refactoring" }'

    Name
    Type
    Description

    type*

    string

    Version type. Only manual and autosave can be created via the API.

    label

    string

    {
        "versionId": "e1a7a2a4-9d5e-4c8f-a1c9-2f7f7f9b1a10",
        "versionNumber": 4
    }

    GET [TENANT_API_URL]/flows/:id/versions

    Return the versions of a flow (metadata only, without the flow descriptors).

    curl "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/versions?limit=10" -H "Authorization: Bearer [ACCESS_TOKEN]"

    Name
    Type
    Description

    type

    string

    Filter by version type: manual, autosave, publish or restore-point.

    limit

    number

    {
        "items": [
            {
                "versionId": "e1a7a2a4-9d5e-4c8f-a1c9-2f7f7f9b1a10",
                "flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
                "versionNumber": 4,
                "type": "manual",
                "label": "Before big refactoring",
                "name": "My Flow",
                "btime": "2026-07-01T09:12:33.001Z"
            }
        ],
        "totalCount": 4
    }

    GET [TENANT_API_URL]/flows/:id/versions/:versionId

    Return a single version including its stored flow descriptor.

    curl "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/versions/e1a7a2a4-9d5e-4c8f-a1c9-2f7f7f9b1a10" -H "Authorization: Bearer [ACCESS_TOKEN]"

    PUT [TENANT_API_URL]/flows/:id/versions/:versionId

    Update the label of a version. Only the flow owner can update versions.

    curl -XPUT "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/versions/e1a7a2a4-9d5e-4c8f-a1c9-2f7f7f9b1a10" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{ "label": "My milestone" }'

    Name
    Type
    Description

    label*

    string

    New version label (max 255 characters).

    DELETE [TENANT_API_URL]/flows/:id/versions/:versionId

    Permanently delete a version. Only the flow owner can delete versions.

    curl -XDELETE "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/versions/e1a7a2a4-9d5e-4c8f-a1c9-2f7f7f9b1a10" -H "Authorization: Bearer [ACCESS_TOKEN]"

    POST [TENANT_API_URL]/flows/:id/versions/:versionId/restore

    Restore the flow to the state stored in the version. Only the flow owner can restore versions. The behaviour depends on the flow's state:

    • Running flow: the version is restored into a draft (an existing draft is replaced), the live flow keeps running unchanged. Publish the draft to apply the restored state.

    • Stopped flow with a draft: the draft is replaced with the version's content; the original flow is untouched.

    • Stopped flow without a draft: the flow itself is restored in place.

    • Draft: when called on a draft flow, the draft is overwritten in place.

    In every case the pre-restore state is snapshotted as a restore-point version first, so nothing is lost — whether the restore replaces a draft or the flow itself.

    curl -XPOST "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/versions/e1a7a2a4-9d5e-4c8f-a1c9-2f7f7f9b1a10/restore" -H "Authorization: Bearer [ACCESS_TOKEN]"

    POST [TENANT_API_URL]/flows/:id/versions/:versionId/clone

    Clone a historical version into a new, independent flow. The request body accepts the same options as Clone Flow (prefix, connectAccounts, ...).

    curl -XPOST "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/versions/e1a7a2a4-9d5e-4c8f-a1c9-2f7f7f9b1a10/clone" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{}'

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

    GET [TENANT_API_URL]/flows/:id/validate

    Validate a flow and return validation errors grouped by component. The validator checks, among other things:

    • Required input fields (including fields defined inside expression-type inspector fields).

    • Components that require authentication but have no connected account.

    • The shape of the per-component errorHandling settings.

    • Components whose type cannot be resolved — an unknown or malformed component type is reported on the component itself rather than failing the request.

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

    [
        {
            "componentId": "a1b2c3",
            "errors": [
                {
                    "keyword": "authentication",
                    "message": "Component is not authenticated. Connect an account.",
                    "componentId": "a1b2c3"
                }
            ]
        }
    ]

    POST [TENANT_API_URL]/flows/:id/test

    Execute a single test run of a flow without the flow having to be running. The endpoint executes the selected component and traverses the flow graph downstream, streaming per-component results in real time as Server-Sent Events (SSE). The flow's stage is not changed and no production state is modified.

    This is the API behind the Test flow button in the Designer.

    curl -XPOST "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/test" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{ "componentId": "76a77abf-d5ec-4b1c-b6e8-031359a6a640" }'

    Name
    Type
    Description

    componentId*

    string

    The component to start the test run from (typically the flow's trigger).

    payload

    object

    When no payload/inputData is provided, the engine resolves the trigger's test data using a fallback chain and labels the entry component's component:output events with a dataSource attribute:

    dataSource
    Meaning

    real

    The trigger's real logic executed — its test(context) method fetched live data, or a live webhook arrived.

    logs

    Data from past runs found in the flow logs (Elasticsearch fallback).

    dummy

    The response is an text/event-stream. Events: test:start, component:start, component:output (per-port output, includes dataSource for the entry component), component:response (captured context.response()), component:error, component:done, component:stop and test:done (carries the overall test status).

    Incoming webhook calls to a stopped flow are automatically forwarded to an active test run of that flow, so webhook-based flows can be tested end-to-end.

    Records produced by a test run are logged through the regular logs pipeline and carry a testRunId field — component outputs, input-queue records, modifier errors, static component calls and the component's own context.log() output. Filter them with the testRunId query parameter of the Logs API, for example GET /logs?flowId=<id>&testRunId=<id>; the ID is delivered in the test:start SSE event. Test runs are also marked as such in the Designer logs panel.

    Setting
    Default
    Description

    TEST_MODE_TIMEOUT

    60000

    Maximum allowed timeout (ms) per test run.

    TEST_MODE_MAX_DEPTH

    50

    DELETE [TENANT_API_URL]/flows/:id/test/:testRunId

    Abort a running test. The testRunId is delivered in the test:start SSE event.

    curl -XDELETE "https://api.appmixer.com/flows/9089f275-f5a5-4796-ba23-365412c5666e/test/1d1f0d02-6f52-4c34-a6b1-7b6f1a9f30aa" -H "Authorization: Bearer [ACCESS_TOKEN]"

    {
        "testRunId": "1d1f0d02-6f52-4c34-a6b1-7b6f1a9f30aa",
        "status": "abort-requested"
    }

    GET [TENANT_API_URL]/flows/:id/components

    Return the component manifests of all components used in the flow. Useful for progressively loading only the manifests a client actually needs instead of the full GET /components catalogue.

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

    Each component in a flow descriptor can define how an unresolved error (an error that remains after automatic retries) is handled. The settings are stored on the component descriptor and configurable via Update Flow or in the Designer (cogwheel → Error handling):

    Field
    Type
    Description

    autoRetry

    boolean

    Whether the engine retries the failed message automatically. Default true.

    maxRetries

    number

    With onError: "errorPort", the error is routed to a virtual error output port that can be wired to any downstream component like a regular output port. The connected component receives the error message (error.message, error.code, error.name, input) plus the flow data (scope) of preceding components, and the corresponding variables are offered in the Variables API and the Designer variable picker. Flows without errorHandling use the default behaviour.

    See Handle Flow Errors for the complete error-handling guide.

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

    Name
    Type
    Description

    id

    string

    Flow ID.

    Name
    Type
    Description

    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.

    Name
    Type
    Description

    command

    string

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

    {
        "flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b",
        // equals the flowId if the background=true is sent in the query
        "ticket": "26544d8c-5209-44ac-9bdf-ef786924b07b"  
    }

    GET [TENANT_API_URL]/flows/{flowId}/coordinator/status

    {
        "status": "completed",
        "stepsTotal":1
    }

    GET [TENANT_API_URL]/flows/:flowId/components/:componentId

    Query Parameters

    Name
    Type
    Description

    enqueueOnly

    Boolean

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

    {
        // Response
    }
    {
        // Response
    }

    POST [TENANT_API_URL]/flows/:flowId/components/:componentId

    Query Parameters

    Name
    Type
    Description

    enqueueOnly

    Boolean

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

    {
        // Response
    }
    {
        // Response
    }

    PUT [TENANT_API_URL]/flows/:flowId/components/:componentId

    Query Parameters

    Name
    Type
    Description

    enqueueOnly

    Boolean

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

    {
        // Response
    }
    {
        // Response
    }

    The notes property holds the definitions of note blocks displayed on the Designer canvas. Notes are purely visual — they let flow authors annotate a canvas with Markdown-formatted text to explain intent, guide end-users, or document template behaviour.

    The Designer writes this object automatically when notes are created or edited. You can also construct or patch it programmatically via the Create Flow and Update Flow endpoints.

    notes is a map of note ID → note object. Each key is a UUID that uniquely identifies the note block.

    Property
    Type
    Description

    x

    number

    Horizontal position of the note block on the canvas (pixels, can be negative).

    y

    number

    A flow becomes an Integration Template when it has a non-empty wizard property. The wizard object defines the configuration form that end-users fill in when they activate an integration — rendered by the appmixer.ui.Wizard widget.

    The Wizard Builder in the Designer writes this object automatically. For programmatic template creation — importing templates, scripting bulk updates, or building custom tooling — you can construct or patch the wizard field directly via the Create Flow and Update Flow endpoints.

    fields is an ordered array of field objects. The Wizard widget renders them top to bottom. Authentication (account) fields are always sorted to the top of the form regardless of their position in the array.

    Every field object shares a common set of properties:

    Property
    Type
    Description

    type

    string

    Field type. See the field types below.

    label

    string

    Exposes a single component input field to the end-user. This is the most common field type.

    source format: <componentId>.<descriptorPath>, where descriptorPath mirrors the config transform path in the flow descriptor — for example a0828f32.config.transform.in.76a77abf.out.lambda.channelId.

    Renders an account/authentication selector for one or more components that share the same auth service. Account fields are always displayed at the top of the Wizard form.

    attrs property

    Type

    Description

    service

    string

    The auth service identifier, e.g. "appmixer:slack".

    components

    string[]

    A field not bound to a specific component inspector input. Useful for injecting arbitrary data into a flow (e.g. via customFields).

    attrs property

    Type

    Description

    type

    string

    Input type, e.g. "text".

    path

    A non-interactive content block — a heading or informational paragraph displayed inside the Wizard form.

    attrs property

    Type

    Description

    format

    string

    Content format, e.g. "header".

    text

    string

    A static image displayed inside the Wizard form.

    attrs property

    Type

    Description

    image

    string

    Image URL or path.

    The following example creates an Integration Template programmatically. The flow has a Scheduler component (76a77abf) and a Slack SendChannelMessage component (a0828f32). The wizard exposes the Slack account selector and two input fields — the scheduled hour and the Slack channel.

    An Integration Template is a flow that has a non-empty wizard property and no templateId property. To list all published templates:

    To list a user's integration instances (flows created from a template):

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

    Query Parameters

    Name
    Type
    Description

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

    Get Flows

    Query Parameters

    curl -XPOST "http://[API-URL]/flows/9089f275-f5a5-4796-ba23-365412c5666e/clone" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-type: application/json" \
      -d '{
        "setOriginFlowId": true,
        "setComponentIdMap": true,
        "connectAccounts": true,
        "additional": {
          "type": "integration-instance-draft"
        }
      }'
    curl -XPOST "http://[API-URL]/drafts/draft-flow-id/publish" \
      -H "Authorization: Bearer [ACCESS_TOKEN]"
    {
        "a0828f32-34b8-4c8d-b6b3-1d82ca305921": {
            "type": "appmixer.utils.http.GetRequest",
            "errorHandling": {
                "autoRetry": true,
                "maxRetries": 5,
                "onError": "errorPort"
            }
        }
    }
    {
      "notes": {
        "ec488c75-b240-4b8a-8939-e05ba4af32a4": {
          "x": -368,
          "y": -128,
          "width": 432,
          "height": 288,
          "content": "## My note\n\nSupports **Markdown** content."
        }
      }
    }
    {
      "wizard": {
        "fields": []
      }
    }
    {
      "type": "inspectorField",
      "label": "Slack Channel",
      "tooltip": "The Slack channel to post into.",
      "placeholder": "#general",
      "source": "a0828f32.config.transform.in.76a77abf.out.lambda.channelId",
      "attrs": {},
      "options": {}
    }
    {
      "type": "account",
      "label": "Slack account",
      "tooltip": "",
      "attrs": {
        "service": "appmixer:slack",
        "components": ["a0828f32"],
        "sharedAccountId": null
      },
      "options": {}
    }
    {
      "type": "customField",
      "label": "Customer ID",
      "tooltip": "",
      "placeholder": "e.g. CUST-001",
      "attrs": {
        "type": "text",
        "path": "customFields.customerId",
        "placeholder": "e.g. CUST-001",
        "searchPlaceholder": null,
        "service": null,
        "components": null,
        "options": {},
        "params": {}
      },
      "options": {}
    }
    {
      "type": "text",
      "label": "",
      "tooltip": "",
      "attrs": {
        "format": "header",
        "text": "Configure your Slack alarm"
      },
      "options": {}
    }
    {
      "type": "image",
      "label": "",
      "tooltip": "",
      "attrs": {
        "image": "https://example.com/wizard-banner.png"
      },
      "options": {}
    }
    curl -XPOST "[TENANT_API_URL]/flows" \
      -H "Authorization: Bearer [ACCESS_TOKEN]" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Slack Alarm",
        "type": "integration-template",
        "flow": { ... },
        "wizard": {
          "fields": [
            {
              "type": "account",
              "label": "Slack account",
              "tooltip": "",
              "attrs": {
                "service": "appmixer:slack",
                "components": ["a0828f32"],
                "sharedAccountId": null
              },
              "options": {}
            },
            {
              "type": "inspectorField",
              "label": "Hour",
              "tooltip": "The hour (0–23) at which the alarm fires.",
              "placeholder": "",
              "source": "76a77abf.config.transform.in.trigger.out.lambda.hour",
              "attrs": {},
              "options": {}
            },
            {
              "type": "inspectorField",
              "label": "Slack Channel",
              "tooltip": "The Slack channel to post the alarm message into.",
              "placeholder": "#general",
              "source": "a0828f32.config.transform.in.76a77abf.out.lambda.channelId",
              "attrs": {},
              "options": {}
            }
          ]
        }
      }'
    GET /flows?filter=templateId:!&filter=wizard.fields
    GET /flows?filter=templateId

    Get Flow

    Path Parameters

    Get Flows Count

    Create Flow

    Request Body

    Automatic account connection

    Update Flow

    Path Parameters

    Query Parameters

    Request Body

    Delete Flow

    Path Parameters

    Clone Flow

    Path Parameters

    Request Body

    Flow Drafts

    Overview

    Create a Draft

    Path Parameters

    Request Body

    Draft Uniqueness

    The API ensures only one draft per flow and type can exist. If you attempt to create a draft when one already exists, the API returns the existing draft's ID instead of creating a duplicate.

    Publish a Draft

    Path Parameters

    Publishing Process

    Publishing Impact

    Publishing a draft will immediately update the original flow. If the original flow is running, the changes will take effect.

    Use Cases

    Flow Versions

    Overview

    Create Version

    Request Body

    List Versions

    Query Parameters

    Get Version

    Update Version

    Request Body

    Delete Version

    Restore Version

    Clone Version

    Validate Flow

    Flow validation also runs automatically before a flow is started (configurable with the FLOW_VALIDATE_BEFORE_START setting; can be skipped per-request with ?skipValidation=true on the start command). A flow whose connectors have no account connected fails validation instead of failing at runtime with "Missing access token".

    Test Flow

    Request Body

    Trigger data resolution

    SSE events

    Test runs in logs

    Configuration

    Abort Test Run

    Get Flow Components

    Component Error Handling

    Start/Stop Flow

    Path Parameters

    Query Parameters (only for the stop command)

    Request Body

    Get the status of the stop flow command

    Send GET request to a component

    Send POST request to a component

    Send PUT request to a component

    Flow Notes

    The notes object

    Note object properties

    Flow Wizard

    The wizard object

    Field reference

    inspectorField

    account

    customField

    text

    image

    Complete example

    Filtering Integration Templates

    For a guided walkthrough of building and publishing templates using the no-code Studio, see .

    Send DELETE request to a component

    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 key, Password, OAuth 1, and OAuth 2.

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

    Authentication Module Structure

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

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

    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.

    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.

    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.

    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:

    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 .

    A field in the auth object can declare a display precondition with the when property, so that it is only shown when another field in the same dialog has (or does not have) a specific value. This is useful when the credentials required depend on the user's choice — for example, a connector supporting multiple AI providers where Amazon Bedrock needs an AWS region while all other providers need only an API key:

    The when property has the structure { op: { field: comparisonValue } }, reusing the syntax of the . The auth dialog supports two operators:

    • eq: The field is shown when the referenced field's value equals the comparison value.

    • neq: The field is shown when the referenced field's value differs from the comparison value. If the referenced field is empty or unset, the precondition is not met — the field stays hidden until the user selects a value.

    Multiple entries (and both operators combined) are evaluated with logical AND. The ./ prefix on the referenced field name (Inspector syntax) is supported and optional — { eq: { './provider': 'amazon-bedrock' } } and { eq: { provider: 'amazon-bedrock' } } are equivalent.

    When a precondition is not met, the field is not rendered in the dialog. It appears immediately once the referenced field changes to a matching value, without reloading the dialog. A hidden field is also excluded from validation and from the submitted credentials — in the example above, connecting an OpenAI account stores only provider and apiKey, never awsRegion.

    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.

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

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

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

    Similar to requestProfileInfo. This property is used to validate if the authentication data entered by the user is correct. For this purpose you can call any endpoint that requires authentication, you can even use the same endpoint as requestProfileInfo. If the data is correct, this function should resolve to any value. Otherwise, throw an error. You can also define validate as an object. In that case, the object has the same structure as the object passed into the axios library. For example:

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

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

    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.

    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:

    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:

    Works exactly the same way as described in the section.

    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.

    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.

    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.

    Works exactly the same way as described in the section.

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

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

    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:

    Works exactly the same way as described in the section.

    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.

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

    Works exactly the same way as described in the section.

    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.

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

    String or an array of strings.

    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:

    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.

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

    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 [TENANT_API_URL]/auth/[service]/callback.

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

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

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

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

    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.

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

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

    To set your OAuth applications secrets, follow the guideline.

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

    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.

    wizard

    object

    Wizard configuration for Integration Templates. See for the full schema.

    notes

    object

    Note blocks displayed in the Designer canvas. See for the full schema.

    Properties to be filtered from the flow model.

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

    connectAccounts

    Boolean

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

    isTemplateInstance

    Boolean

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

    setOriginFlowId

    Boolean

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

    setComponentIdMap

    Boolean

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

    additional

    Object

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

    Must be true to map component IDs for publishing

    connectAccounts*

    Boolean

    Recommended true to copy account connections

    additional*

    Object

    Must include type field set to "automation-draft" or "integration-instance-draft"

    Optional human readable label (max 255 characters).

    Maximum items returned. Default is 50, maximum is 100.

    offset

    number

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

    Optional trigger output data keyed by output port (e.g. { "out": {...} }). When provided, the component's own logic is not executed.

    inputData

    object

    Optional input data for testing a single component with explicit input.

    options

    object

    Optional timeout (ms, capped by the TEST_MODE_TIMEOUT server setting) and maxDepth (capped by TEST_MODE_MAX_DEPTH).

    Sample data generated from the trigger's output port JSON schema.

    manual

    Data supplied in the test request itself (payload or inputData).

    Maximum flow graph traversal depth.

    TEST_MODE_MAX_TIMEOUT_DELAY

    120000

    Maximum delay (ms) for context.setTimeout() in test mode.

    Optional override of the number of retries (bounded by the system retry backoff configuration).

    onError

    string

    What happens once retries are exhausted or disabled: errorPort, stopFlow or storeUnprocessed (default — store the message in ).

    Vertical position of the note block on the canvas (pixels, can be negative).

    width

    number

    Width of the note block in pixels.

    height

    number

    Height of the note block in pixels.

    content

    string

    Full CommonMark Markdown displayed inside the note block. Supports headings, bold, italic, inline code, fenced code blocks, links, images, tables, and lists.

    Label shown to the end-user.

    tooltip

    string

    Optional tooltip text.

    placeholder

    string

    Optional input placeholder text.

    source

    string

    Path to the bound component input: <componentId>.<descriptorPath>. Not used by all field types.

    attrs

    object

    Type-specific attributes. Shape varies by type — see below.

    options

    object

    Additional display or behaviour options.

    IDs of flow components that should use the selected account.

    sharedAccountId

    string

    Optional. When set, the Wizard pre-selects a specific shared account instead of prompting.

    string

    Data path the value is written to.

    placeholder

    string

    Input placeholder.

    searchPlaceholder

    string

    Placeholder for searchable select inputs.

    service

    string

    Optional linked auth service.

    components

    string[]

    Optional linked component IDs.

    options

    object

    Custom options passed to the field renderer.

    params

    object

    Custom parameters passed to the field renderer.

    The text content to display.

    Flow Notes
    Build and Publish a Template
    // 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;
            }
        }
    };
    module.exports = {
    
        type: 'apiKey',
    
        definition: {
    
            auth: {
                provider: {
                    type: 'select',
                    name: 'Provider',
                    options: [
                        { value: 'openai', label: 'OpenAI' },
                        { value: 'amazon-bedrock', label: 'Amazon Bedrock' }
                    ]
                },
                apiKey: {
                    type: 'password',
                    name: 'API Key',
                    // Shown for every provider except Amazon Bedrock.
                    when: { neq: { provider: 'amazon-bedrock' } }
                },
                awsRegion: {
                    type: 'text',
                    name: 'AWS Region',
                    // Shown only when Amazon Bedrock is selected.
                    when: { eq: { provider: 'amazon-bedrock' } }
                }
            },
    
            // ...
        }
    };
    {
        contact: {
            email: 'appmixer@example.com',
            name: 'Appmixer example',
            // More properties here...
        }
        // There can be more properties here as well...
    }
    'use strict';
    
    module.exports = {
    
        type: 'apiKey',
    
        definition: {
    
            tokenType: 'apiKey',
    
            accountNameFromProfileInfo: 'appId',
    
            auth: {
                appId: {
                    type: 'text',
                    name: ' APP ID',
                    tooltip: 'Log into your account and find Api key.'
                },
                apiKey: {
                    type: 'text',
                    name: 'REST API Key',
                    tooltip: 'Found directly next to your App ID.'
                },
                cert: {
                    // just to show, that a 'textarea' input is supported as well
                    type: 'textarea',
                    name: 'TLS CA',
                    tooltip: 'Paste text content of <code>.crt</code> file' 
                }
            },
    
            // In the validate request we need the appId and apiKey specified by the user.
            // All properties defined in the previous 'auth' object will be
            // available in the validate call. Just use {{whatever-key-from-auth-object}}
            // anywhere in the next object. Appmixer will replace these with the
            // correct values.
            validate: {
                method: 'GET',
                url: 'https://acme.com/get-some-records/app_id={{appId}}',
                headers: {
                    'Authorization': 'Basic {{apiKey}}'
                }
            }
        }
    };
    
    validate: {
        'method': 'GET',
        'uri': 'https://api.apify.com/v2/users/{{apiUserId}}?token={{apiToken}}'
    },
    
     validateErrCallback: err => {
    
        if (err?.someNestedObject?.message) {
            throw new Error(err.someNestedObject.message);  // message for the UI
        }
        throw err;
    }
    'use strict';
    
    module.exports = {
    
        type: 'pwd',
    
        definition: {
    
            // As opposed to the 'apiKey' module, the 'auth' property inside the 
            // definition object is optional for the 'pwd' type. If omitted, it will 
            // have the following structure. If you want to change name of the field,
            // change the content of the tooltip or add another field, you can do
            // that here. 
            auth: {
                username: {
                    type: 'text',
                    name: 'Username',
                    tooltip: 'Username'
                },
                password: {
                    type: 'password',
                    name: 'Password',
                    tooltip: 'Password'
                }
            },
    
            // the only mandatory property for the 'pwd' authentication type
            validate: async context => {
            
                const { username, password } = context
                const { data } = await context.httpRequest.post(
                    // the URL for the username/password authentication
                    'https://service-server-api-url/auth',
                    { username, password }
                );
                
                // verify authentication works
                if (!data.isValid) {
                    throw new Error("Invalid username/password combination.");
                }
                
                // if the API does not return a token for exchange, then return here.
                // in such case, the components using this authentication module
                // will have to use the username/password to perform each request.
                // There will be properties context.auth.username and 
                // context.auth.password available in the context object in a component.
                
                // if the API returns a token and expiration then return them using 
                // the following way - object with 'token' and 'expires' properties.
                const { token, expiresIn } = data;
                
                // the token and expiration (if provided) will be available in a
                // component's context object under context.auth.token and
                // context.auth.expires
                return {
                    token,
                    // expires can be a Date - token expiration date (timestamp),
                    // or number/string with seconds representing a token lifetime.
                    // Appmixer will use this to request a new token (using stored
                    // username and password) when the current one is about to expire
                    expires: expiresIn
                };
                
                // if the API returns a token that does not expire, just return
                // that token 
                // return { token };
            }
        }
    };
    
    'use strict';
    module.exports = {
    
        type: 'pwd',
    
        definition: {
    
            validate: async context => {
    
                await context.httpRequest.get('https://postman-echo.com/basic-auth', {
                    auth: {
                        username: context.username,
                        password: context.password
                    }
                });
            }
        }
    }
    'use strict';
    module.exports = {
    
        async start(context) {
    
            await context.httpRequest.get('https://postman-echo.com/basic-auth', {
                auth: {
                    username: context.auth.username,
                    password: context.auth.password
                }
            });
        }
    }
    'use strict';
    const OAuth = require('oauth').OAuth;
    const Promise = require('bluebird');
    
    module.exports = {
    
        type: 'oauth',
    
        // In this example, 'definition' property is defined as a function. 
        definition: context => {
    
            let trelloOauth = Promise.promisifyAll(new OAuth(
                'https://trello.com/1/OAuthGetRequestToken',
                'https://trello.com/1/OAuthGetAccessToken',
                context.consumerKey,
                context.consumerSecret,
                '1.0',
                context.callbackUrl,
                'HMAC-SHA1'
            ), { multiArgs: true });
    
            return {
    
                accountNameFromProfileInfo: 'id',
    
                authUrl: context => {
    
                    return 'https://trello.com/1/OAuthAuthorizeToken' +
                        '?oauth_token=' + context.requestToken +
                        '&name=AppMixer' +
                        '&scope=read,write,account' +
                        '&expiration=never';
                },
    
                requestRequestToken: () => {
    
                    return trelloOauth.getOAuthRequestTokenAsync()
                        .then(result => {
                            return {
                                requestToken: result[0],
                                requestTokenSecret: result[1]
                            };
                        });
                },
    
                requestAccessToken: context => {
    
                    return trelloOauth.getOAuthAccessTokenAsync(
                        context.requestToken,
                        context.requestTokenSecret,
                        context.oauthVerifier
                    ).then(result => {
                        return {
                            accessToken: result[0],
                            accessTokenSecret: result[1]
                        };
                    });
                },
    
                requestProfileInfo: context => {
    
                    return trelloOauth.getProtectedResourceAsync(
                        'https://api.trello.com/1/members/me',
                        'GET',
                        context.accessToken,
                        context.accessTokenSecret
                    ).then(result => {
                        if (result[1].statusCode !== 200) {
                            throw new Error(result[1].statusMessage);
                        }
                        result = JSON.parse(result[0]);
                        // get rid of limits for now
                        // may and will contain keys with dots - mongo doesn't like it
                        delete result.limits;
                        return result;
                    });
                },
    
                validateAccessToken: context => {
    
                    return trelloOauth.getProtectedResourceAsync(
                        'https://api.trello.com/1/tokens/' + context.accessToken,
                        'GET',
                        context.accessToken,
                        context.accessTokenSecret
                    ).then(result => {
                        if (result[1].statusCode === 401) {
                            throw new context.InvalidTokenError(result[1].statusMessage);
                        }
                        if (result[1].statusCode !== 200) {
                            throw new Error(result[1].statusMessage);
                        }
    
                        result = JSON.parse(result[0]);
                        if (result['dateExpires'] === null) {
                            return;
                        }
                        throw new context.InvalidTokenError('Invalid token.');
                    });
                }
            };
        }
    };
    
    'use strict';
    const request = require('request-promise');
    
    module.exports = {
    
        type: 'oauth2',
    
        // function definition is used in this case because of the 'profileInfo'
        // property that we want to locally store and reference between
        // the 'requestAccessToken' and 'requestProfileInfo' functions.
        definition: () => {
    
            let profileInfo;
    
            return {
    
                accountNameFromProfileInfo: context => {
    
                    return context.profileInfo['email'] || context.profileInfo['id'].toString();
                },
    
                authUrl: 'https://app.asana.com/-/oauth_authorize',
    
                requestAccessToken: context => {
    
                    // don't put params into post body, won't work, have to be in query
                    let tokenUrl = 'https://app.asana.com/-/oauth_token?' +
                        'grant_type=authorization_code&code=' + context.authorizationCode +
                        '&redirect_uri=' + context.callbackUrl +
                        '&client_id=' + context.clientId +
                        '&client_secret=' + context.clientSecret;
    
                    return request({
                        method: 'POST',
                        url: tokenUrl,
                        json: true
                    }).then(result => {
                        profileInfo = result['data'];
                        let newDate = new Date();
                        newDate.setTime(newDate.getTime() + (result['expires_in'] * 1000));
                        return {
                            accessToken: result['access_token'],
                            refreshToken: result['refresh_token'],
                            accessTokenExpDate: newDate
                        };
                    });
                },
    
                requestProfileInfo: () => {
    
                    return profileInfo;
                },
    
                refreshAccessToken: context => {
    
                    // don't put params into post body, won't work, have to be in query
                    let tokenUrl = 'https://app.asana.com/-/oauth_token?' +
                        'grant_type=refresh_token&refresh_token=' + context.refreshToken +
                        '&redirect_uri=' + context.callbackUrl +
                        '&client_id=' + context.clientId +
                        '&client_secret=' + context.clientSecret;
    
                    return request({
                        method: 'POST',
                        url: tokenUrl,
                        json: true
                    }).then(result => {
                        profileInfo = result['data'];
                        let newDate = new Date();
                        newDate.setTime(newDate.getTime() + (result['expires_in'] * 1000));
                        return {
                            accessToken: result['access_token'],
                            accessTokenExpDate: newDate,
                            // Some services return a new refresh token, if so, it can
                            // be returned here and Appmixer will replace the old one.
                            refreshToken: result['refresh_token'] 
                        };
                    });
                },
    
                validateAccessToken: {
                    method: 'GET',
                    url: 'https://app.asana.com/api/1.0/users/me',
                    auth: {
                        bearer: '{{accessToken}}'
                    }
                }
            };
        }
    };
    
    authUrl: 'https://www.dropbox.com/oauth2/authorize'
    'use strict';
    const TENANT = 'common';
    
    module.exports = {
    
        type: 'oauth2',
    
        definition: {
    
            scope: ['offline_access', 'user.read'],
    
            scopeDelimiter: ' ',
    
            authUrl: `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/authorize`,
    
            requestAccessToken: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
    
            refreshAccessToken: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
    
            accountNameFromProfileInfo: 'displayName',
    
            emailFromProfileInfo: 'mail',
    
            requestProfileInfo: 'https://graph.microsoft.com/v1.0/me',
    
            validateAccessToken: {
                method: 'GET',
                url: 'https://graph.microsoft.com/v1.0/me',
                auth: {
                    bearer: '{{accessToken}}'
                }
            }
        }
    };
    
    module.exports = {
    
        type: 'oauth2',
    
        definition: () => {
    
            // telling Appmixer it cannot refresh it sooner than 30 seconds 
            // before access token expiration
            refreshBeforeExp: 30,
            refreshBeforeExpUnits: 'seconds',   // 'minutes' by default
            
            // the rest is business as usual
            authUrl: 'https://www.acme.com/oaut2/authorize'
            
            ...
        }
    }
    module.exports = {
    
        type: 'pwd',
    
        definition: {
    
            // the only mandatory property for the 'pwd' authentication type
            validate: async context => {
            
                const { username, password } = context;
                const { data } = await context.httpRequest.post(
                    // the URL for the username/password authentication
                    'https://service-server-api-url/auth',
                    { username, password }
                );
                
                // verify authentication works
                if (!data.isValid) {
                    throw new Error("Invalid username/password combination.");
                }            
                return true;
            }
        }
    };
    'use strict';
    const GoogleApi = require('googleapis');
    const Promise = require('bluebird');
    
    module.exports = {
    
        type: 'oauth2',
    
        definition: initData => {
    
            return {
    
                // The auth.js 'definition' object can have an optional 
                // 'connectAccountButton' property, which has to contain the 'image'.
                connectAccountButton: { image: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAX4AAABcCAYAAABpyd51AAAAAXNSR0IArs4c6QAAHvtJREFUeAHtXQmYFNW1/qvXWRnWYQYHBMdRhHEQkEXDaCCIGty+MRHUCCbikodxS4TkkfceWdSEGI1PeZFgVBQjfu/Bp4njU8HlgYphBJR9G0dkGVaZjZnptd653V3dVdVV001P90w3nPt91V1113P/e+vcc8899xbAjhFgBBgBRoARYAQYAUaAEWAEGAFGgBFgBBgBRoARYAQYAUaAEWAEMhEBKU6i440XZ3YcjRFgBBgBRiBFCMix8o3F0K2UgSV0ibix4scqj8MZAUaAEWAEUoOAYPji8ocun1kxZoxc+FtH3fH24B6DLv6dZHFMkCyW/maZsD8jwAgwAoxA9yMg+/2HZb/7o6bDm3++4b8mfkUUCeYfNQMwY/yC6Q8pGHLpOkmy9Or+6jAFjAAjwAgwAvEiIMv+hhP7Ph37+aIpX1KaKMnfZpKRLX/gyAWC6Y89x4qHvutAvx5C48PODIGjTX488ZYb676MwtgsCfszAowAI5ASBIh39ywoHvF7yvxmuqKYkhE3F7MAh8WS9S1BETN9gUJsJwZGgRU7RoARYATSAYEQDxdMKUqzY8r4Jau1UBDPkn78TchYxY8Vx2QEGIHUIhDi4afE+M1UQKmllHNnBBgBRoARSCYCgpfHLfEbzQSSSQznxQgwAowAI5B6BAQvj4vxC1KiIqaePi6BEWAEGAFGIMkIGPJyluyTjDJnxwgwAoxAuiPAjD/dW4jpYwQYAUYgyQgw408yoJwdI8AIMALpjgAz/nRvIaaPEWAEGIEkI8CMP8mAcnaMACPACKQ7Asz4072FmD5GgBFgBJKMADP+JAPK2TECjAAjkO4IMONP9xZi+hgBRoARSDICzPiTDChnxwgwAoxAuiPAjD/dW4jpYwQYAUYgyQgw408yoJwdI8AIMALpjgAz/nRvIaaPEWAEGIEkI8CMP8mAcnaMACPACKQ7Asz4072FmD5GgBFgBJKMQNp/cMV3uB7umk/h2b4Z3j27IDecgL+5CbBIsPTqA0vvPrAWFsExehwcYy+h575JhoizYwQYAUbg9EIgLRm/LMtwfbgS7W+ugOeLDaaI++sPQFzerZvg+uDdQDzb0OHImT4DzgkTTdNxACPACDACZzICacf43ev/iZZF/wnfl7sTahfvjq1omj8XtrKhyL3rJ3CMHJNQPpyIEWAEGIHTFYG00fHLbjean3kcjXN/kjDTVzeSd/cOND48GydfeBay368O4ntGgBFgBM5oBNJC4vc3NaLxF/fBu3N70huj9ZXn4a3bg4JfP570vDlDRoARYAQyEYFuZ/z+E9+gYc5s+OpqU4Ofw4ns629KTd6cKyPACDACGYhAtzJ+2eNB4388nFKmX/CbP5LFz9gMbBommRGID4ExI+245TwLPF7AbpOx5n03VhyNLy3HOnUE8gfY8PNKK+yEN4iDHtvqxoIt8qln1I0pupXxtyz8I7zbNsdVfdt5F8Ax7lsQVjuWXr0Bsvzxk2mnd9d2uNd9Au/2Ldp8SNI/3Zn+ZHrhp1xgxVm9JNH/AOqIza1+1H3lwxsbvNhyUgtJ4KmfDUt+4ERJNj35ZLzzRhsW7MicTnvzNVmYVWENVKVhnxtzXvYgRXNFA/CS75WM+gy7wI6KwVKYuOyvPcT4U9em+f2suONSG0YWWZCl4iAnjvmwfqsXi7ec3mtqhWdZMb40UnGvw0eMX4wCmeMi1HcxzW4y0xTmmrGcbdiFyLv7ftiHVxhGddJgkHvbLHjIpLNl0VPBgeQ0Z/rl5Xb821QH+gb5nwaXvn0sGDLQhkmVTuzf6sK9b3jRrIoxeZw9yPSFn1XClZfbiPF7VDHS+NZhxTUhpi+o7DnQjqv6ebAwU6XbJNXH7dO2mZD8U+Mk3HmjE9PPN+h4VGDfAgvKSu2YfoUPK/7uwsLa1A0+qalfnLnq8G3X4R9nLt0arVsYv9/nRcvTj8WsePb0mcj90Y8hWWIbH4mBoeeTi3Dyr38ObuY6TdU7Yyqz8DuaZsbjSoY7sayHhKkkFSvOqdwo/7aIpKh4Zc6/jJZI1TKHbFNK9fWRMP/uHFT2ockcMReb1Y/nnmrDq0YzOdM8kxVgwYL7sjE6L478sq2ompaDc1e24sGa05T5xwFDOkeJzVFTQf2Jd+AsX0092XxKmH3TbcibNTsupq+QKFltyBO2+6cp00c/O35pwPRbjosptgeb9vmFtkfjsgY6sGRyZKBYvdmLBlWM9ev0KVSB6XZLou0nX0UYibfRj23qyqQbvbHoiaM+BTnBTGyBJpSQZ4+VaWrC58wyYvoyqRW9+HSnF4daIu2iUFBxRRaqcpUn/k8nBLpF4pfrl8J54Qk6aqENJ1cMIV29Vg7NuqYqwMDTCah0oGXqOBrYNIT4sezFNiw+qPJ0WPDID7MxnqRExZVcaEPpKl9AF96814MbH/WgOFdCy0lZowZS4qfz/8K/tWKhQ0IxZNS705nS+GiLVR/9hMat94ivmE7Fyi914MpCbRbtRzyY/5wbNSrv8nIHHrnOruqjFtx6lRUrlmegLkRVr9PxtssZv+xpgHzszQCWtv7t6PHDnTj55tnw7C4I+El5+cidde/piHXS63Rok1vL9EUJbj/mLXVh+f1O9FRKpKn3BHqoFdJxrgU3D7PAEQo7RgvB1YYLgRKqKu2YONiCoJAp49B+H17+wIvWs0nXXighwHfb/FhBi3nKOkLxACumnBVUHzmIOb9fQwNOTyseoLWEoaR2CjhaVN68iXTzCSwCjhlqw7D8EPFeGe9u9KE+9JiysmmguXmENYxZ8zekw9bpr8uJrkuILje9Ucf2eAnTEFGhv8qRNpyjvG1E9wqiW2BmXB/CfowVTq+EUrEIH3YSLhprg7uFPJp9eNVkUd7bRuG5VsyZTGUqmFO/WP+FB4tN0oSLMLiZcZlCeCiQ9Gv3EdOv1cXdssWNx2nBd/7YyAyz5yASOhAUOnTRA4+TCZcJ51hRlBPsGx6ic8cuH14K4WOURu0nFpqnU9sMK7SAujk5GceP+vHBpx6sijkbTKyPq8uPdZ/f04LpF1OfDdFnI/oO1/vw+movarpRcNG1aKxqdD5cPrIc8LvCGUlZfuTeWIf2tYVoX12M7O/fCkueVq4NRz7Db5yR9ymIhJmW5qQPOxplXEwwen0SskCMOSQplg6zY9YVkWb37nOh+mVdRsLyZyZZ/iijQwj3Mlo0rrzIjmM0qwgvLNN6zfotLig2VVO+7cQMlYXJ6EFeDDnfFrQ6UrVf2WAbpozx4KEXohmIKpruVsKM650YFsZBhqOuFYtDL3iqys6nReRZV6h0LEfIXLJWLXpb8ECVE0MUaodLqH5OGz7zalU4tcexrW2odpvUB2Q1c4WT2i3aVVziRMDMgXDfvCOCuzpmxaQsVOeRxY3ak/IUC69XkSXUvbTmowyWmihGD7lkKFAcGrBD4evfN7ekWrOW1u+I8Yff4GwLhlI/qtUxufKRDvxqih0kE+gcMXGis+o7Piz7bxcW741WIQUTWDDvtixMGqilTYSVDQTGj3Lgtp1k3LBca9wQLqwTfTycR4c3Eh6YloVrS6O16cL4YvxYB9Z/0I45a83V3R1m38nACAfoZEbxJpcbPo6KKlHbZV96BLbB1NmrpkeFG3l89mXnpo90uCdGDYnqdUZFpY1fk0v7EhSNcmJBo1HnkTFvYasx3ToeH2WR0NOG5XeqZgv6XOhF7qv2oxdaCJiK01uYlBHTN3N5xXY8eo0P096Mvy3bBANRScFqfpKqsptrfdhP854SpSJ9rBgDT0TNQTMg4jURV2jFZApfpfiQVFqs3NO/97gXq0OEm9VH10yq1HHcRjH9SJqetObz+FV+3Pp2/JhHUtOdz4d/dGSzftKLexb7UaKMkx45SrItHePEUyrhQ5O/8kAWT9NvzcHgv7diXlR5UlwLzSXnO/H63RbctUgnXHSyjyskmv9LmEeL8pNU6tbouBJGT8zGktx2zCQ1bFc787cyRZTILYpsGF2Ao6ICUnZoNSs6WOMzd1m75jmRh7fn5sBOJo2Z4lZt9+HhUVaV9BzsPO+M8WF1jQfL1tOUWs0JE6jYnO8ZMH1S52w76EchSSp9dbMAEiTjcvv3edFks2BYsVYC6jvcjkpi/GviyiWxSJ0v24eN9TJKFMnXasHYfkBNSJ0zldZQtC+SFROHSlgVUquUn6uVvg/RQKKoxgxr1CpjJ6kDepCqp2ygFq+GIz4cJT7hoZ+9hokjnqLebTRNLCO1nNoVldswhhi/Wj+vDlff51P5pCWMOFLTHYw8Gd7Vk6rFdEZBM4hHDZj+flok/oZwrdDVd/x1WZi6S8yOIkVVXZ8VZV3kbfFh2wEZhaRaKlIJBugTLVykqo8rFFZOdkYx/XYyRKindu1FQkNP1TtUMtaJmWtbsaSLLbW0/VWhPJX/rgOmuUv5o03DUhHQQIub/RQdaCoKSHaetDD727VWzL9Ey21teVZMmiguoKXRh3UbPHh+bUT3HTcZZDV0uW4R7xhNl6fRdDno3HjgtmxcazC9Ni9DuwBdTNP456Y5ImoIetmHEmdZE1Mfa16CeUjyyv54jx/XFiu4S7iQmDmIwQESLj1Hy5wFPUOHU9wdQdxGa2aWMjZuizG9p6nLnBeCUuAjD+ZifJiRyfifl9rxqooJGtfdj5cWt2FJaGAqJl3Li1X2yOBE0vRFhHlNHJgLlY2oRZhRuGWYzCWNSdH5Vl1F+0/Ufj6i9fkIrfln2/HirQ7VYGPBjClWVCuzQho4bh2uxfsQ7Ve5lfarBJ0Ld5Ip6fTSyGDXt8KBqnfbsELglpI+rq6QFfeo1jhESN26dswKS/U0GyDji4j6TMIUstRbkugMTF30KdxrETyFhAlH9TaaJ3X0Nw9LQcgJYvyZ5taQXvCxNV6YzXfyCsQAkIWlNJt5xMD0s6P6jim3RhiyiEiLeA+Emb7wkPGnl9uwLSbjEXGDbjfRq7Y6qq/1gtTbGncK2WnSxXpIZtk1O3wazAcKxi8cKanLwwrtoFfAm5h9eeCRFmT7R5iQWHz/OJbIHM5GCi2shz2QF8eEeNvK9jDTFynraYPetkQxJ44fZvqUV8thnTRPg8jTs3OwfHY2Xou6cvDGbCdor2HIWTBxiAoL8v30DS2twupszkqFiQeT9aWF89JQDuUVNtWgQJ7UR+8JM/1gpMWvtWGTpr4WVNK6i3Cp6OPBUoO/pbRYXaT2aPSomL4IkPHImx5NXyo6T6sKVCdP1X3XM/5U1SSBfE9G1pgTSN19SVatcWHq461YscmHFjMySIU1njZ7vXGLomw1ixjx16th6sjm32jK3nYKKsnoXaQyth7umgE3qWUf9WKXaoTK6m8NMKNyUvMY8H2AGOLkAYQt/Q8KS+xA+0Gy5ohAfsp3KhJOIa2MbQlinmPXMuqsfAmKUVWAALLGGVQgoSft2hU7d7UX7TsgQWSAMliRhcug8CBAqWm94HUDK6NaUlvWqfsYGROMCKUbPUTLsuo2Gi3ektXU5+oMgMKzgulS0cfVDTGWmLjG0RhWRaq1m2lACF/najEFWY11uBygyTA5D+rBPDk5xsrFRmabntAcVB/XfVjvk9LnHtm6BkhpaUnOnKbcC99spwuoJInosuE2jD2XrCl0/S5vMG3gosW8mXFMJfWLo20Gm3KSUQt9OcnIM948Ei9bxrp9MioUFQIxdDLMQG/V8QUNX3lwsK8dwwIjgYSRZDaLOq2O/MvdWoYUL92djUcq8MScjkPYiHmfTTmpV+p0UaLKCQ9WtNArBJXwQEl9+Juo2MKD/CnRkPCASYxRDB7kp2+/ZpM+uqdBK1zkhVS6+vSp6uPhatEaw+zrwk+mN5oJimms5AXEarPklaTk5DzLlPHLzeuVWF3y3zsvgxm/CqE1pEsWl3BT6Yyeuyq1UmjJCHtci3kusi8XOmvFtWln3Ir3Gfv/4S4fZoUP5yLGPtoG+uRz2O34pwefXEQ22+cHMSwabMNMWtCOOBmfbdMypEhYet7VHvEH1BJq01ANk2rw4a8rXegT4iRuWpCeMsmBIp0AYlS7lsPm9v36+OHBQxewZ78xnsokQxcdadnHCavwGKcnOEXPXc74pbxyyC2fG1Znd8M+DPK0IcceG4ZLy2L3rKNNMnaTTtLICXNOZTu8UXja+dGGnPnX2FBAzNhDrWZvpxMBSbepV8VUkxqo+ms/qmmBLPyy0gJqPIt5Tt25PfEridIOrZQQVE+M/9jVZNkUyn30ZU466E4pyo9PaFPXajqr96Hzg8jZCm2o6hkZSNHixbtdbL2hUJfw/7GglB7uSw4bbjzbhQV7lRxJrVKjlhAkDJ9AjN/oFSa1UVjap+R5AXWZN2ojmJKz+l+tIVL7n1tC+BpuQFTHitx3eR+nNR1hEdeh9SCdqHs8QmKX3HU94+/5LciHlmoqRycs4+W2c7Go9Wzcvfst/GjYjZpwo4fffD/cFY2CA37PvucyZfz9SS9pERsIMsTlkwhVGZY2iWiauvcxYPyB6uwVA4IjsqGIPM0kJnX19dLQgEEkrdIOSnYhBGhj3HayTagUm8wFLGGmT5YvR2i3rohGi8ARm39idMSxgges0Y7evQlYWok8u9Op6xyi43LaybtAv+kvHhppT5t6iIDJ5i6x61i7Y5l245qYEgV09wZ9VK9rbychULhU93FXuygnwlcO7XDjJ4pFUoCC9PhRz0O7hCKp8EbIEklKIdfit2FO81j8uXUY7WeUsHTH39Hi7rxY5CV749X0Epq5saWqt9YsUhr5Nx/yaw5Xg9WGeytNmo92JfbT0W4mMamjrd2pnR31He5QWWSEYtJmJO1Lqc7hdL+X8dHeEEai+6i6V91O5cGHj2gtQO2CB6wBe3T4quOY3Xf/rEtGNRkRqF3WQCeenWj2/kjoYRYkdpQLJX/YWXDTpOjIVZNpR284Dt3QhreVIcnlw61aWooq7LRZTufI5PMGZS0mFLSJ9sAIl+o+vpZm22pXVOHEnfqXUUSgNaKn6bTTeeWRQUKdLtX3JpwjdcVK9p7w9r4yUMAebw/c3nA5PnJHDKCaPSfx9KZXOk1A9edeHKZjC8zcOFoIzShHOyLXHNFSXFaZjVfIJn7qALK0IM6eTwevTb3EgeU/cmim1HRGBnbEYbNdTx+T2K8pwoKH7svCTMpfuFKywX+to129mrSn58M/N/siUmu4C8lYuznywr+/RcucgkjQyaIGFiwdoyTjc401DunPJ9gCB+yV5nacMpmhNaQ+pGOhNK7skiwsv436Xj86MC/U9yaPseOVn2WjTCdlRB5lvKqTzktGZeHZq6hOgdwlzKQP7czW2elvoo8KNYdKr9/igXZ93IpfPJiFm0N9tJi+jrXkx86wOi6QjBarltUGM0h1H6/fqKdPwvQ7czBvpCVsDVVJ39N4jWgeRjusJ11HZtcju575d7mqR8DvKbwJKw9swIKWEXCp58vBtqFzUN5FLun57xtxW8jn1P720KLRovd0PVWVhZNqPZK22Wea+9OrLlxCh68pOmZBfxEx44fE1UFl9q/zYE0H4ZEgOoRtgx+/GKWSB0gymXF7DmZEIp3Rd82kRtvns4PM9CNO6O5VA2stfYVKvRYgInrp6OzVkRRx3+mtUIQEuVQc1uP24q7HXXHpx+MuzDSijAf/5kb17ap1I4orPoTz0J0x5iS0QYuOUwq7LWIQuTgHFao1gDI6emQpXUINFMWQyA5+geZMfz+eed+rPfKBTmebRX30dhpvldlVuEC6WfO/6iMbUt3H/fjDu178hdaCIk7CpKtp09bVQVWXOkTEueg86kxkltqVTvWGd12xOf2r8DfrDYZMX6Hi5R1v4JkvlsIvRyQpJayj/y37fJj7ajvp8sxjXUfWGA7dQqZ57DQKIal/2mIX6JDMuN2xWjqoKrxrMHayVW+34x06nqArXUQijF2qns2cSlqj3E89vR8bj2jxOUSLvppFduLW63WzM7NjGmLV583PTDryKfSBpNT7IG1Eoq2vGk2NUcZqP9rw8cQz7VilkcFoEPlLO3ZrTIOCifQMUdhuPrbYrcWWom6pceGJddEAGDF9sWt2vm6mleo+XrvRFdhkqYZCuY+qIw1sc18zaWMlUQr+u4Xx20g//fCoO2JWZ8mO1zHrvV9i07GdMePubT6IR2qexf0rn8eJVnMgc2l54ZZLT/11j0lAV0WgjUQzf9+K59Z6DD9+oZDRQHrRZa+1Yhp1KmWaLMJa1YdG0nN71O5lGQteoPw3qFQaSqb0v5s++EKfVo049T35BhfPIsFNBi94RFcSjKfhC5GkhnetmvK0X6xKddkKQf+nWTuS8YlOBy7ivUVSf8TRHgCTYxo6qo9I30yHL4md2urcAvmqZhxdVW+xA/h62ji4bBN9zKeDRmug2c37tGP7hifbUW20XEe6/nuepHxIX2+4A53W53ZvctGMRj9oRBCtXtWOH7ziQp2JHb/4OJHo/5GjEiJpxT6BzvTx2O8QIDZZ3vAizW50QkKYCnFM9jqKs9Ct2RMRDk/xjZFySQxKhZMeaTkgyn7vX1OnTHzss78E1Drx1PGCXqWYMGAUhvcuQ++sAtr57sGBlsPYT9f2b2rxcf0Gas6gJGZtK0P24dmw+HpFZT1roh03kx48Ve47jxr19FSVFtTrD+1Lh4DR5hjQhjQn2eLX1fs7d1ib+NAJba6pp8X2yUMtaDwuo6CPhDo6EbXWbkM1qZvCNlWkcrifVA5bUldFzlkgQG0yWZwJRAO3YPR7qI27+0M0xf0sGE4boxrbqH9Q32tsorUkMq1UCxqxG0/CmFILmSnLaKT+luXx4/ODp5aHoGMU9c8jop/2oD1zZAhRG+s17MI+LtbextHhcy5hWUQ4tR/30zlJ2lljbJwSi/H+vDzaOAUx/9TIDlEzj8SyTyzVz0b+ELsbvsLm47tiZrD9RC3EFY/zZe/GyZJ/DzB/W/vQcJIRZJ74vbH6yXU4OCNvmkliryEJKnmOjry9J4dOP6TDs+jrXksU6Taw2VrCHPX+ACrU2+Bnpp888M1zooF4ldIW5rG6NCRwCqfJJvz4CaH+S6eVdsYJOsIfvomLnq7t4+IdTbe261bGb7fa8fiEufiXD3+F2savO9P2UWllWxNaB/wOzm9ugrPhuxhEEsGvv5fV8UaKqFzOPI87b1G+rUqnIt6ei0l0DMFHu2mdhSwQvj3ajiLdZOmLTzWCxJkHGNc44xDgPm6wiN7VrSjUNosm/gr3rf4ttpHKJqlOkuHq8xpKChvw2OQ7kJdlpNlKaokZn1mv0CfwlIqUDLZj+mDlSfdPC1NPRn0kQxeHHxmBNEOA+zjQLYu7+n5Q4MzH4km/xU1lZO+UZDeheDQWXTsdRXS4FLvYCCx4jk79rI1tSXWIFh3vooUpjTVL7Ow5BiPQ7QhwH08DiV/pBQ5S+whLn8sGXIynvniJdP97laCE/ns6e+AOOvphWtl3IWXQ0QwJVTapiejUTzrP/CU6Z37GeBtGllggJCRhKid2Qx89QJ/eI4uialqAY8cIZCYC3Me7Vcdv1GnGFY3A0v5/wMqvPyHJcyU2HN1qFM3Ur39OX9xy3lRUlU5Blo1sN9klhEAznbq48O3OLbolVDAnYgS6CIEzuY+nHeMXbW6RLLjy7AmBq/7kUaw99Dm2kOXPzhN1OOFqQpM7uI2kt7MAvbJ6oDC7D0YVDsd4GjSG9Cjpom7DxTACjAAjkJkIpCXjV0NZnNuPpPcrApfan+8ZAUaAEWAEEkOAVzwTw41TMQKMACOQsQgw48/YpmPCGQFGgBFIDAFm/InhxqkYAUaAEchYBJjxZ2zTMeGMACPACCSGADP+xHDjVIwAI8AIZCwCzPgztumYcEaAEWAEEkOAGX9iuHEqRoARYAQyFgFm/BnbdEw4I8AIMAKJIcCMPzHcOBUjwAgwAhmLADP+jG06JpwRYAQYgcQQYMafGG6cihFgBBiBjEWAGX/GNh0TzggwAoxAYggw408MN07FCDACjEDGIsCMP2ObjglnBBgBRiAxBJjxJ4Ybp2IEGAFGIGMRYMafsU3HhDMCjAAjkBgCzPgTw41TMQKMACOQsQiYMX7Z7/UcE7U62uTP2Mp1NeGMVVcjzuUxAoyAGQIhHi4bhRsxfhHR52k7tkEkeOItNzN/I+R0foLpC6zYMQKMACOQDgiEeLiPaIli/pIBgWIwKBgwbtbF513zh+UWiz3fIA57MQKMACPACKQpArLP07TzHz+98WDN8+uJxEa6NKobqwnd1uYDG9xtJ776uMdZF/WXrNkFFqst2yQuezMCjAAjwAikAQI+j+sbd/OBT3f946dzD218ZSeR1ExXlCrCSOIX5DvoEpJ+H7oK6MqiS8wEzOJTEDtGgBFgBBiBbkRAqHSEZN9Ol5Dyj9NlyPhtFGDkvOR5MhTgon/B+MXsgBl/CBT+YwQYAUYgzRAIrM8STYLxt9AleLjg5VGuI0YuwuyhSwwQLPFHwccejAAjwAikDQKKxC+YvSd0RS3sCmo7YvwiXDgRR7kCHvzDCDACjAAjkJYICEavXGlJIBPFCDACjAAjwAgwAowAI8AIMAKpRuD/AebmxjtTus0OAAAAAElFTkSuQmCC' },
    
                scope: ['profile', 'email'],
    
                accountNameFromProfileInfo: function(context) {
    
                    return context.profileInfo.email;
                },
                
        ...
    }
    

    type

    definition

    Authentication mechanisms

    function, object, or a URL string

    API key

    auth (object)

    Conditional fields (when)

    Available since Appmixer 6.5.

    requestProfileInfo () (optional)

    accountNameFromProfileInfo ()

    validate ()

    Password

    HTTP Basic Authentication

    OAuth 1

    accountNameFromProfileInfo ()

    requestRequestToken ()

    requestAccessToken ()

    authUrl ()

    requestProfileInfo () (optional)

    validateAccessToken ()

    OAuth 2

    accountNameFromProfileInfo ()

    authUrl ()

    requestAccessToken ()

    requestProfileInfo () (optional)

    refreshAccessToken ()

    validateAccessToken ()

    scope

    scopeDelimiter

    refreshBeforeExp

    refreshBeforeExpUnits

    OAuth 2 redirect URI

    Context

    async context.httpRequest

    async context.log(string severity, object)

    Custom "Connect account" button

    Setting OAuth 1,2 secrets

    context
    NodeJS module
    Inspector's conditional fields
    examples of real connectors
    API Key
    here
    here
    Function, object or a string
    API Key
    validate
    Asana auth.js module
    API Key
    JIRA auth.js module definition
    the JIRA auth.js module definition
    API Key
    OAuth 1
    axios
    Google
    Google auth.js
    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.
    Test Flow API
    Flow Wizard
    Flow Notes
    Unprocessed Messages
    function, object or string
    function or string
    function, object or string
    function or string
    function, object or string
    function, object or string
    function, object or string
    function, object or string
    function or object
    function or string
    function, object or string
    function, object or string
    function, object or string
    function, object or string
    function, object or string