# 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](/getting-started/build-your-automation-hub).

<figure><img src="/files/LJeTEaujC7AfCVe8COBS" alt=""><figcaption></figcaption></figure>


# 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](/getting-started/integrations)
2. [Customize and Share Your Hub](/getting-started/customize-and-share-your-hub)
3. [Embed Your Automation Hub](/getting-started/embed-your-automation-hub)
4. [Monitor Hub Performance](/getting-started/monitor-hub-performance)

***

**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
* Allow or Disable customization of pre-built templates by end-users


# Build and Publish a Template

{% embed url="<https://www.youtube.com/watch?v=PZyzOWA_uJw>" %}

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
* Publish your Template to your end-users for immediate use.
* See a preview of how your template will be shown to your end-users

## Explore our demo content or build from scratch

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

## Template Overview - 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.<br>

<figure><img src="/files/MmwcQJuP6kZdNP05AcC4" alt=""><figcaption></figcaption></figure>

## Create an Automation Template

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](/getting-started/use-app-events) represents the simplest method for sending data to Appmixer.

{% hint style="info" %}
You can also pick ready-to-use templates from our Get Started section on the Dashboard
{% endhint %}

<figure><img src="/files/1Pfbp1hyPqVUnaaChsIo" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/fu94njyQmRiUVCvlDCfP" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/p3pccB5ta3lyTikvF5ml" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/V9jvWI0zgHEQGvvNdams" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/Wz6FIxFdOpD3vZ9Ghppt" alt=""><figcaption></figcaption></figure>

## Select Configuration Fields to Collect from your End-Users

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:

<figure><img src="/files/yCxj5MLKQUlwHlPedYg8" alt=""><figcaption></figcaption></figure>

#### Predefine values for your users

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.

#### Apply data transformations

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.

<figure><img src="/files/lV00gBRqrZCue5PKd2Fd" alt=""><figcaption></figcaption></figure>

Finally, edit the name of the automation template:

<figure><img src="/files/zycu8p37rMB24BOCpAbZ" alt=""><figcaption></figcaption></figure>

## Refine the template wizard

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 `Edit Wizard` 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

### Advanced wizard options

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.

#### Share account with all template users

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.

#### Hide tooltip

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

#### Disable variables

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

#### Restrict variables

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

#### Disable modifiers

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

<figure><img src="/files/0BT8QEYqAtgoLjZk3EIf" alt=""><figcaption></figcaption></figure>

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

## Test your Template

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.

<figure><img src="/files/Ze0fYz9KN6CTgDFnBiDP" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/j0jhmWy9ivtVAYfizp8C" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/SyyohcBRPb2167J2TJLn" alt=""><figcaption></figcaption></figure>

## Publish your Template to your End-Users

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.

#### Set target users

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

* User
* Scope
* Domain

#### Update existing instances

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.

{% hint style="info" %}
Please note that it is not possible to force updates that would touch end-user configuration, i.e. values and variables in configuration fields.
{% endhint %}

<figure><img src="/files/uqwdfqvGEHm3EzhXb6ID" alt=""><figcaption></figcaption></figure>

#### Preview the Template

Please note, if Appmixer has not yet been [embedded into your product](/getting-started/embed), you can easily open a preview of the Automation Hub by clicking the `Preview` button in the `Templates` section or on the `Dashboard`.

<figure><img src="/files/i3OmNjjjGBOms080iTOY" alt=""><figcaption></figcaption></figure>


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

{% hint style="info" %}
Use the `Preview` button on the Dashboard, in the Templates section or in the Hub Settings to check how the Automation Hub looks.
{% endhint %}

## Design and Feature Customizations

Use the options in the Automation Hub Settings section to adjust the page structure and design.

### Text and toggles in the Hub

<figure><img src="/files/2grcz1dyTMX4OB2F6edB" alt=""><figcaption></figcaption></figure>

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

### Organizing Automations into Categories

<figure><img src="/files/olNakjtNUyvqfSKjmwnC" alt=""><figcaption></figcaption></figure>

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

### Theme

<figure><img src="/files/aEoPz6Xr9WQYAI4M3tVh" alt=""><figcaption></figcaption></figure>

Use the Theme Options section to customize colors and fonts.

### Feature Customizations

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

Customization of automations in the Hub has two levels.

<figure><img src="/files/b1bX4XbwV4mrrhveZTIB" alt=""><figcaption></figcaption></figure>

#### **Allow customization of pre-built automations**

<figure><img src="/files/4rO8JFhkmC7a0mmfu0nR" alt=""><figcaption></figcaption></figure>

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`

#### **Allow creating custom automations from scratch**

<figure><img src="/files/BiM2CUsP0iV2EDh5nfYj" alt=""><figcaption></figcaption></figure>

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.

{% hint style="info" %}
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.
{% endhint %}

## Sharing 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


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

```javascript
<!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="https://TENANT_ID.appmixer.ai/appmixer/package/appmixer.js"></script>

  <script type="module">
    const API_BASE_URL = 'https://api-TENANT_ID.appmixer.ai';
    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>
```

### Authenticating your end users

[Read more about authenticating your users](/getting-started/embed#authenticate-your-end-users) in the dedicated documentation section.


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

***

<figure><img src="/files/vy4gqYJrasXKHR1q6cOV" alt=""><figcaption></figcaption></figure>

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

***

#### How to use this

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

***

#### Access analytics via API

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.


# Build and Run an Automation

{% embed url="<https://www.youtube.com/watch?v=XCNCgNfp6rk>" %}

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

## Automation Overview

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

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

<figure><img src="/files/H4QfOZKhfZ2eot8GXO00" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/VQuvSuuv77zmDjtdh07m" alt=""><figcaption></figcaption></figure>

As a consequence, we received a notification in Slack:

<figure><img src="/files/S7Ofq83oEY4TJz1VqMqQ" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/p1UIaOPk2ITZLBtdRpTh" alt=""><figcaption></figcaption></figure>

## A Step-by-step Guide

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

### Select a trigger

Every automation begins with a trigger, which initiates the automation flow. Typically, triggers can capture data from external sources—like a Webhook trigger or a `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.

<figure><img src="/files/mUJJoRLUtYD0G3mLRJ36" alt=""><figcaption><p>Select a trigger in your Automation</p></figcaption></figure>

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

<figure><img src="/files/HA35758Zy37Eqaocyftl" alt=""><figcaption><p>Authenticate and configure your trigger</p></figcaption></figure>

### Add Actions

Click `Add Step` on the side of the `New Entry` trigger and find Slack. Pick the `Send Channel Message` action. Authenticate with your Slack account and select a Slack channel to direct your notifications:

<figure><img src="/files/qLnH86T0hrWFpOH7cP2z" alt=""><figcaption></figcaption></figure>

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:

<figure><img src="/files/sIljMJbIxfn6koOb57EI" alt=""><figcaption></figcaption></figure>

Similarly, add the `OpenAI - Send Prompt` action. Configure the model, set the response type to `json_object`, and define the prompt as follows:

<figure><img src="/files/TzHOFAyyr4djat92dNVh" alt=""><figcaption></figcaption></figure>

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:

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

### Create new variables and transform data with Modifiers

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

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

<figure><img src="/files/hJH5KsNJC4ouHLJWPW4M" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/flePw2FKsZSQXe0TZhDj" alt=""><figcaption></figcaption></figure>

### Finalize our Workflow Automation

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

<figure><img src="/files/oaTpxWvdtM2RWhfwMQX9" alt=""><figcaption></figcaption></figure>

## Start and Monitor Your Automation

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

<figure><img src="/files/ky4riniPLzGFwmUUdjut" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/K1YRzcTAYX5JZRHFL96C" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/ZbxT0oBrFFhADX0ZYBmE" alt=""><figcaption></figcaption></figure>

This action will produce a filtered list of logs:

<figure><img src="/files/Z8pVaEDjDsQN4fDXpMYf" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/2BZeO6LJvXWqQu4iKlZ6" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/EDfjKhg3S4U0BYVO1HRi" alt=""><figcaption></figcaption></figure>


# Build and Run an AI agent

{% embed url="<https://www.youtube.com/watch?v=XELzEbU68R4>" %}

### What This AI Agent Does

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

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

Sounds useful? Let’s build it!

### Step 1: Setting Up the AI Agent

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d185e6e59820d568c221e0_1%20(8).png" alt=""><figcaption></figcaption></figure>

‍

#### Choosing a Trigger

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

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d175c80fabd74b8e89b6fa_2.png" alt=""><figcaption></figcaption></figure>

‍

#### Adding the AI Agent

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

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d175e71c21e3c611f51474_3.png" alt=""><figcaption></figcaption></figure>

‍

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

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

‍

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.

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d1b724f378a449792e6e24_4%20(4).avif" alt=""><figcaption></figcaption></figure>

‍

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d1b72fd2ab76af2bd2bbd8_5%20(4).avif" alt=""><figcaption></figcaption></figure>

‍

#### Handling the Agent’s Responses

The agent has two output ports:

* **Out**: This sends the agent's answers to the chat widget (or any other tool, such as sending a message to Slack).
* **Tools**: This is where we connect the agent to our data and services, whether first-party or third-party.

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d2b9528d0c206b24ea4d7c_6.1.png" alt=""><figcaption></figcaption></figure>

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d176ba7d564628f33d76a6_7.png" alt=""><figcaption></figcaption></figure>

‍

### Step 2: Adding Skills to the AI Agent

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

#### Connecting to External Tools

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

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

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d176cf6ebee8b126b98366_8.png" alt=""><figcaption></figcaption></figure>

‍

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.

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d176e42ecb347195e971d2_9.png" alt=""><figcaption></figcaption></figure>

‍

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

```htmlbars
Send emails to the product team.
```

‍

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d176f742422747823a8014_10.png" alt=""><figcaption></figcaption></figure>

‍

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d177077f6a11ef02c4633e_11.png" alt=""><figcaption></figcaption></figure>

‍

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

```htmlbars
Read the support tickets submitted by users last month.
```

‍

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d17789ff64905432bf22d5_12.png" alt=""><figcaption></figcaption></figure>

‍

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

```htmlbars
Read the scopes and responsbilities and contact details of product teams.
```

‍

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d177976ebee8b126ba312b_13.png" alt=""><figcaption></figcaption></figure>

‍

### Step 3: Testing and Deploying the AI Agent

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

For example, we can:

* Ask it about common issues in the mobile app from past support tickets.
* Request product improvement suggestions based on user feedback.
* Instruct it to send these suggestions to the mobile app team.

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d177a536eb7d1225a5bd05_15.png" alt=""><figcaption></figcaption></figure>

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

<figure><img src="https://cdn.prod.website-files.com/5ef0a48c75d5b78296c95323/67d1a89c49985a4ca9a057ad_14%20(4).avif" alt=""><figcaption></figcaption></figure>

### Expanding Your AI Agent’s Capabilities

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

For instance, you can:

* Integrate **vector database** for large-scale data retrieval.
* Connect additional execution tools like task assignment in **Asana**, **ClickUp**, or **Jira**.
* Create multi-agent workflows where each AI agent specializes in a different function, such as:
  * Scraping websites
  * Generating marketing content
  * Analyzing market trends
  * Writing product specifications


# 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](https://github.com/clientIO/appmixer-connectors) , 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."

<figure><img src="/files/hSvYLDiFPNm8CUMjugnD" alt=""><figcaption></figcaption></figure>

## Install Appmixer CLI

First, you need to install the [Appmixer CLI](/appmixer-cli/appmixer-cli) tool, which enables you to build, test and deploy custom connectors. You can install the tool using `npm`:

```bash
$ npm install -g appmixer
```

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

<figure><img src="/files/5PI8OO0HrvpoYkYACiyw" alt=""><figcaption></figcaption></figure>

```bash
$ appmixer url https://api.[YOURTENANT].appmixer.cloud
```

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

```bash
$ appmixer login your@admin.com
Password:
```

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

## Create your Component

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

```bash
$ 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."
```

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:

```
$ tree appmixer
appmixer
└── boredapi
    ├── core
    │   └── GetActivity
    │       ├── GetActivity.js
    │       ├── component.json
    │       └── package.json
    └── service.json

3 directories, 4 files
```

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

### Define Input and Output

We can now proceed to edit the `component.json` manifest file, adding an input field to allow users to specify the type of random activity they wish to receive. This `type` field corresponds directly to the `type` query parameter of the [Bored API endpoint](https://www.boredapi.com/documentation).

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:

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

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

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

The `schema.properties` section defines a [JSON Schema](https://json-schema.org/) for our input while the `inspector.inputs` section [defines the form ](https://docs.appmixer.com/getting-started/pages/-LATDlWTegDtvBmHEaZx#properties.inspector)to collect input from the user.

<figure><img src="/files/p2Xgmj1hxydx93xOfVg6" alt=""><figcaption><p>Type input</p></figcaption></figure>

<figure><img src="/files/Y62V8u4LpgIhSS34DPFX" alt=""><figcaption><p>Output of GetActivity consumed in the connected SendEmail component</p></figcaption></figure>

## Call 3rd Party HTTP API

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

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

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

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

## Test your Component

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

```bash
$ appmixer test component appmixer/boredapi/core/GetActivity \
-i '{ "in": {"type": "recreational"} }'
```

Below is the console output from our test:

<figure><img src="/files/qD9QuMjGAQ5SkEAyseoK" alt=""><figcaption></figcaption></figure>

## Publish your Component

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

```bash
$ 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
```

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

<figure><img src="/files/ZYpJXejlrMdkNElwj0QJ" alt=""><figcaption></figcaption></figure>

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.


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

<pre class="language-html"><code class="lang-html"><strong>&#x3C;script src="https://TENANT_ID.appmixer.ai/appmixer/package/appmixer.js">&#x3C;/script>
</strong></code></pre>

{% hint style="info" %}
Note that `YOUR_TENANT`is the name of your tenant. For example, if the URL of your hosted version of Appmixer is [https://my.eminent-emu-12345.appmixer.cloud](https://my.eminent-emu-70007.appmixer.cloud/), then `YOUR_TENANT`is `eminent-emu-12345`.
{% endhint %}

Please refer to our [Installation](/appmixer-ui-sdk/installation)page for additional methods to integrate the Appmixer SDK into your page

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

```javascript
const appmixer = new Appmixer({ baseUrl: 'https://api-TENANT_ID.appmixer.ai' });
```

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

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

{% hint style="info" %}
Although creating virtual users directly from client-side code is feasible, we recommend enhancing the security of your Appmixer tenant by configuring the `API_USER_CREATE_SCOPE` system setting to the "`admin`" string. This ensures that only admin users have the capability to add new users. To create virtual users, you should use the `POST /user` API endpoint, accompanied by an access token from an Appmixer admin user, from your backend application code. For further details, please consult our documentation at [Appmixer Virtual Users](/tutorials/appmixer-virtual-users).
{% endhint %}

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:

```javascript
function generateSecureUsertoken(length = 22) {
  const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  return Array.from(crypto.getRandomValues(new Uint32Array(length)))
    .map((x) => charset[x % charset.length])
    .join('');
}

generateSecureUsertoken()  // ODQMwnwGeZQeXTV5sj3AsR
```

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

## Embed the Automation Hub

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.

```javascript
<!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="https://TENANT_ID.appmixer.ai/appmixer/package/appmixer.js"></script>

  <script type="module">
    const API_BASE_URL = 'https://api-TENANT_ID.appmixer.ai';
    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>
```

## Embed Integration Marketplace (legacy widget)

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:

```html
<div id="integrations-placeholder"></div>
```

```javascript
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();
```

<figure><img src="/files/pRVdNfDtkapA9gY94Xfj" alt=""><figcaption><p>Default - not yet themed - embedded Integration Marketplace</p></figcaption></figure>

## Embed Automation Designer

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

```html
<div id="flow-manager-placeholder"></div>
<div id="designer-placeholder"></div>
```

```javascript
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();
```

<figure><img src="/files/pwbXtnMQGhJJyJRgqHOb" alt=""><figcaption><p>Appmixer Designer Embedded in your SaaS product</p></figcaption></figure>

<figure><img src="/files/roh6ZFdkOr1Ya7xFYtGf" alt=""><figcaption><p>Appmixer Flow Manager Embedded in your SaaS product</p></figcaption></figure>

## Custom Strings and Localization

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

```javascript
appmixer.set('strings', {
    ui: {
        flowManager: {
            search: 'Search Automations',
            header: {
                title: 'Automations',
                buttonCreateFlow: 'Create Automation'
            }
        }
    }
});
```

<figure><img src="/files/s4q3O072FnsYJ1IVXGBL" alt=""><figcaption><p>Changed UI Strings in embedded Flow Manager</p></figcaption></figure>

For more information on customizing text strings and a comprehensive JSON object listing all the text placeholders available for customization, please refer to the [Custom Strings](/customizing-embedded-ui/custom-strings) page in our documentation.

## Custom Theme

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

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

```

<figure><img src="/files/a1oTRtIp8SVqmO5BNWAl" alt=""><figcaption></figcaption></figure>

For more detailed information, please refer to the [Custom Theme](/customizing-embedded-ui/custom-theme) page in the documentation.

## Demo Applications

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

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


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

<figure><img src="/files/Ln6T13jEeuIi4dRfA9Gl" alt=""><figcaption></figcaption></figure>

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 (![](/files/TbVV6PCSPubyoONcYBuz)) to view all the flows associated with that user, including integration instances and automations.

<figure><img src="/files/DMAmDiQ2cbnuZgYnmzmW" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/nCCXPqQnK0RVNLXuSbYh" alt=""><figcaption></figcaption></figure>

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.


# Access Appmixer REST API

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

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

```
https://api.YOUR_TENANT.appmixer.cloud
```

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

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

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

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

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

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

Find the full documentation to the REST API [here](/api/authentication).


# Install and Update Connectors

Manage the modules available in the system.

The "Connectors" section of the [Appmixer Backoffice](/appmixer-backoffice/getting-started) 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.

<figure><img src="/files/hpM7gBclxOZ6JRhvjgCh" alt=""><figcaption></figcaption></figure>

## Connector Details

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

<figure><img src="/files/VkhAEq9OdYiouPLLRSkV" alt=""><figcaption></figcaption></figure>

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

### Installing a connector

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

<figure><img src="/files/YOxVn2cGqSMX5c0U8Zjz" alt=""><figcaption></figcaption></figure>

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](/getting-started/services#custom-oauth-credentials)" section of the documentation for guidance.

<figure><img src="/files/Rg9Se9mHIZ34pAmZ6sL1" alt=""><figcaption></figcaption></figure>

### Access Control

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

<figure><img src="/files/3HjVNEbFWjPC7ybpsuBs" alt=""><figcaption></figcaption></figure>

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

### Removing a connector

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

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

<figure><img src="/files/ZhSn8IKqH78hNYceJksF" alt=""><figcaption></figcaption></figure>

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

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

Failing to stop flows that depend on a connector before its removal will lead to errors in the running flows when the connector components are triggered. These errors will be reported to the [System Webhook](/getting-started/system-webhooks), 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/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.

## Installing Connectors from your filesystem

When creating custom connectors for your Appmixer tenant, there are three methods available for uploading them. First, you can utilize the Appmixer CLI to package and publish your connectors; for detailed instructions, refer to the "[Build a Custom Connector](/getting-started/custom-connectors)" section. Alternatively, you can publish your connector as a zip archive through the Appmixer REST API; for this method, consult the "[Apps endpoints](/api/apps)" 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.

<figure><img src="/files/3nYjQmf1triubd1eG21p" alt=""><figcaption></figcaption></figure>

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

## Upgrading a Connector to a New Version

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

<figure><img src="/files/WbUtp4DMBcRLnptBkLdL" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/SyYn2nE1jevVwGTiVtqy" alt=""><figcaption></figcaption></figure>

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.

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

### Major Version Upgrade

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

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

<figure><img src="/files/Tk0d5oQhjxKNiBoPWMJE" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/p5oMAnQF66v9DJ0HPPqR" alt=""><figcaption></figcaption></figure>

Before proceeding with connector upgrades, it's recommended to configure the [System Webhook](/appmixer-backoffice/system-configuration) `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.

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


# 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](/appmixer-backoffice/getting-started) interface:

<figure><img src="/files/zIF40dQfkiDP7N8Jcckv" alt=""><figcaption></figcaption></figure>

## Custom OAuth Credentials

{% hint style="info" %}
For detailed information on configuring specific connectors, please consult the [App Registration](broken://pages/-MLCAo3id2r0BFBhIsTS) 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.
{% endhint %}

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:

<figure><img src="/files/nSPVE9CrjZEVbJi78FZa" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/6Wvq9ZtiQPE4qYREK1pS" alt=""><figcaption></figcaption></figure>

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.

<figure><img src="/files/8urU9JGlUT9jDkkDvSwI" alt=""><figcaption></figcaption></figure>

Following that, add the `clientSecret` you received from Slack.

<figure><img src="/files/mzZlsefhuhOPpk6LgF9l" alt=""><figcaption></figcaption></figure>

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

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

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.
{% endhint %}

## Domain Verification

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

To facilitate this process, the [Appmixer Backoffice](/appmixer-backoffice/getting-started) 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.

<figure><img src="/files/o9nTJbE0N91Jhr8Mia9u" alt=""><figcaption></figcaption></figure>

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

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


# Handle Flow Errors

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

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

<figure><img src="/files/fge2GkHXuLh0uMmTQT07" alt=""><figcaption></figcaption></figure>

To manage errors within running integrations or automations, configure the `WEBHOOK_FLOW_COMPONENT_ERROR` system variable with a custom URL in the [Appmixer Backoffice](/appmixer-backoffice/getting-started) 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.

<figure><img src="/files/j2wwLGpubGXaDjrGLW2T" alt=""><figcaption></figcaption></figure>

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

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

## Recommendation for End-User Error Notification Content

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

<figure><img src="/files/9bHiAmjxnK9KhkXuBdRe" alt=""><figcaption><p>An example email notification to your end-users.</p></figcaption></figure>

## Example: Data Validation Error

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

![](/files/-MOVxhf8Bib6v1Ry87Rw)

![Log viewer in Designer.](/files/-MOVyX-XVJCVJq0ymJaz)

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:

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

## Example: Network Error

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

![](/files/-MOW-e_FjAh3WmPTNGGU)

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:

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

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.

## Storage Quota Notifications

When [Database Limits](/appmixer-self-managed/configuration/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.

### Storage Quota Webhook Events

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

#### WEBHOOK\_STORAGE\_QUOTA\_WARNING

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

**Payload example:**

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

#### WEBHOOK\_STORAGE\_QUOTA\_SOFT\_EXCEEDED

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

**Payload example:**

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

#### WEBHOOK\_STORAGE\_QUOTA\_HARD\_EXCEEDED

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

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

### System Capacity Webhook Events

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

#### WEBHOOK\_SYSTEM\_CAPACITY\_WARNING

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

**Payload example:**

```json
{
  "event": "system_capacity_warning",
  "physicalCapacityBytes": 107374182400,
  "dataBytes": 85899345920,
  "utilizationPercent": 80.0,
  "maxUtilizationPercent": 95.0,
  "state": "yellow",
  "timestamp": "2026-04-23T10:00:00Z"
}
```

#### WEBHOOK\_SYSTEM\_CAPACITY\_CRITICAL

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

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

### Notification Cooldown

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.

### Use Cases

**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](/appmixer-self-managed/configuration/database-limits).


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

<figure><img src="/files/PaVq85exAyqaRCSmztEv" alt=""><figcaption></figcaption></figure>

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

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

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

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

<figure><img src="/files/cZu0CGEdurc5QF1gGTTj" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/ouaqFfZZKQMDtGbUHCfK" alt=""><figcaption></figcaption></figure>

## Triggering OnAppEvent using Appmixer SDK

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

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

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

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.

## Triggering OnAppEvent using HTTP requests

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

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

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


# End User Guide

## Knowledge base

Please visit the [Appmixer Knowledge base](https://kb.appmixer.com/) for end-user tutorials.


# Authentication

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

## Sign-in User

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user/auth`

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

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

#### Request Body

| Name                                       | Type   | Description                                                                                                              |
| ------------------------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------ |
| password<mark style="color:red;">\*</mark> | string | Password.                                                                                                                |
| username                                   | string | Username. If the email is not provided in the body, a username is required. Hence, a username or email must be included. |
| email                                      | string | Email. If the username is not provided in the body, an email is required. Hence, a username or email must be included.   |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Create User

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user`

Create user. By default, this endpoint is open (does not require authentication). This can be changed by setting the API\_USER\_CREATE\_SCOPE [system configuration](/appmixer-backoffice/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" }'`

#### Request Body

| Name                                       | Type   | Description    |
| ------------------------------------------ | ------ | -------------- |
| password<mark style="color:red;">\*</mark> | string | Password.      |
| email<mark style="color:red;">\*</mark>    | string | Email address. |
| username<mark style="color:red;">\*</mark> | string | Username.      |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}

{% tab title="429 Rate Limit Exceeded" %}

```javascript
{
    "error": "Too many signup attempts for this email address. Please try again later."
}
```

or

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

{% endtab %}
{% endtabs %}

### Rate Limiting

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

{% hint style="info" %}
Rate limiting only applies to unauthenticated signups. When an authenticated admin creates a user via the API, rate limiting is bypassed.
{% endhint %}

#### Configuration

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](/appmixer-self-managed/configuration#user-signup-rate-limiting) in the configuration guide for more details.

#### Error Handling

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

{% hint style="warning" %}
For bulk user creation or migrations, use an authenticated admin account to create users. This bypasses rate limiting entirely.
{% endhint %}

## Get User Information

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user`

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

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}


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

## Overview

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 [Analytics API](https://github.com/Appmixer-ai/appmixer-docs-gitbook/blob/6.4/api/analytics.md))
* **Shareable Links**: Enable public access via email-based authentication with OTP tokens

## Hub Settings

### Get Settings

<mark style="color:blue;">`GET`</mark> `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.

```bash
curl -XGET "http://[API-URL]/automation-hub/settings" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK Settings found" %}

```javascript
{
  "id": "default",
  "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"
}
```

{% endtab %}

{% tab title="200: OK No settings" %}

```javascript
{
  "id": "default",
  "settings": {},
  "createdAt": null,
  "updatedAt": null
}
```

{% endtab %}
{% endtabs %}

**Field Filtering:**

* **Admin users**: See all settings including `sharing.allowedEmails`
* **Regular users**: Sensitive fields are automatically filtered from the response

### Update Settings

<mark style="color:green;">`POST`</mark> `http://[API-URL]/automation-hub/settings`

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

```bash
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"
      }
    }
  }'
```

#### Request Body

| Name                                       | Type   | Description                                          |
| ------------------------------------------ | ------ | ---------------------------------------------------- |
| settings<mark style="color:red;">\*</mark> | object | Settings object with flexible schema                 |
| settings.sharing.allowedEmails             | array  | Email addresses allowed to access Hub via share link |

{% tabs %}
{% tab title="200: OK" %}

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

{% endtab %}

{% tab title="400: Bad Request" %}

```javascript
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "\"settings.sharing.allowedEmails[0]\" must be a valid email"
}
```

{% endtab %}

{% tab title="401: Unauthorized" %}

```javascript
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Missing authentication"
}
```

{% endtab %}

{% tab title="403: Forbidden" %}

```javascript
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient scope"
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**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).
{% endhint %}

### Delete Settings

<mark style="color:red;">`DELETE`</mark> `http://[API-URL]/automation-hub/settings`

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

```bash
curl -XDELETE "http://[API-URL]/automation-hub/settings" \
  -H "Authorization: Bearer [ADMIN_TOKEN]"
```

{% tabs %}
{% tab title="204: No Content" %}
Settings successfully deleted.
{% endtab %}

{% tab title="401: Unauthorized" %}

```javascript
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Missing authentication"
}
```

{% endtab %}

{% tab title="403: Forbidden" %}

```javascript
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient scope"
}
```

{% endtab %}
{% endtabs %}

## Shareable Links

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.

### Architecture

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

### Request Login

<mark style="color:green;">`POST`</mark> `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.

```bash
curl -XPOST "http://[API-URL]/automation-hub/share/request-login" \
  -H "Content-type: application/json" \
  -d '{ "email": "user@example.com" }'
```

#### Request Body

| Name                                    | Type   | Description                        |
| --------------------------------------- | ------ | ---------------------------------- |
| email<mark style="color:red;">\*</mark> | string | Email address (max 255 characters) |

{% tabs %}
{% tab title="200: OK Auto-send enabled" %}

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

Email will be sent automatically with the activation link.
{% endtab %}

{% tab title="200: OK Manual mode" %}

```javascript
{
  "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.
{% endtab %}

{% tab title="403: Forbidden" %}

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

The email is not on the allow list configured in hub settings.
{% endtab %}

{% tab title="429: Too Many Requests" %}

```javascript
{
  "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.
{% endtab %}

{% tab title="500: Internal Server Error" %}

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

Email sending failed (SMTP or Cloud Email API error).
{% endtab %}
{% endtabs %}

**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`)

### Activate Link

<mark style="color:blue;">`GET`</mark> `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.

```bash
curl -XGET "http://[API-URL]/automation-hub/share/activate?code=a1b2c3d4e5f6"
```

#### Query Parameters

| Name                                   | Type   | Description                     |
| -------------------------------------- | ------ | ------------------------------- |
| code<mark style="color:red;">\*</mark> | string | Activation code from email link |

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "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.
{% endtab %}

{% tab title="400: Bad Request" %}

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

Each activation link can only be used once.
{% endtab %}

{% tab title="404: Not Found" %}

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

The activation code is invalid or the link has expired.
{% endtab %}

{% tab title="429: Too Many Requests" %}

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

Rate limit exceeded for IP address.
{% endtab %}
{% endtabs %}

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

### Refresh Link

<mark style="color:green;">`POST`</mark> `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.

```bash
curl -XPOST "http://[API-URL]/automation-hub/share/refresh" \
  -H "Content-type: application/json" \
  -d '{ "email": "user@example.com" }'
```

#### Request Body

| Name                                    | Type   | Description                        |
| --------------------------------------- | ------ | ---------------------------------- |
| email<mark style="color:red;">\*</mark> | string | Email address (max 255 characters) |

{% hint style="info" %}
**Refresh vs Request Login**

Both endpoints function identically but serve different use cases:

* `/request-login`: Initial access request
* `/refresh`: Token renewal for existing users

Both share the same allow list validation and rate limiting.
{% endhint %}

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

## Configuration

### Environment Variables

#### Email Configuration

**SMTP (Option 1):**

```bash
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
```

**Cloud Email API (Option 2):**

```bash
APPMIXER_CLOUD_EMAIL_API=https://api.appmixer.com/email
APPMIXER_CLOUD_EMAIL_API_KEY=your-api-key
```

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

#### Activation Link Settings

```bash
# 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
```

#### Rate Limiting

```bash
# 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
```

## Use Cases

### Public Marketplace Access

Enable public users to access your Automation Hub marketplace:

```bash
# 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
```

### Custom Hub Theming

Configure custom theme and branding:

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

### Programmatic Allow List Management

Dynamically manage email access:

```bash
# 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"
        ]
      }
    }
  }'
```

## Security

### Email Allow List

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

### Rate Limiting

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 Link Security

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

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

## Audit Logging

All hub settings operations are automatically logged:

**Settings Update:**

```
Automation hub settings updated.
```

**Settings Delete:**

```
Automation hub settings deleted.
```

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](/appmixer-backoffice/audit-logs) for more information.

## Error Handling

### Common Errors

**Email Not Authorized (403)**

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

**Solution**: Add email to allow list via settings API

**Rate Limit Exceeded (429)**

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

**Solution**: Wait 60 seconds before retrying

**Invalid Activation Code (404)**

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

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

**Link Already Used (400)**

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

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

### Email Sending Failures

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


# Flows

## Get Flows

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows`

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

#### Query Parameters

| Name                  | Type   | Description                                                                                                                                                                                                                                                                                                                              |
| --------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| filter                | string | Filter flows by their property values. Example: "userId:123abc" returns only flows who's owner is the user with ID "123abc" (i.e. shared flows are excluded). Note that you can also search on nested fields. This is especially useful with the `customFields` metadata object. For example: "filter=customFields.category:healthcare". |
| sharedWithPermissions | string | Filter flows by their sharing setting. Example: "read,start". All possible permission are currently "read", "start", "stop".                                                                                                                                                                                                             |
| projection            | string | Exclude flow object properties. Example: "-flow,-thumbnail".                                                                                                                                                                                                                                                                             |
| sort                  | string | Sorting parameter. Can be any flow object property followed by semicolon and 1 (ascending), -1 (descending). Example: "mtime:-1".                                                                                                                                                                                                        |
| pattern               | string | A term to filter flows containing pattern in their *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.                                                                                                                                                                                                                                                                               |

{% tabs %}
{% tab title="200 Flows successfully retrieved." %}

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

{% endtab %}
{% endtabs %}

## Get Flow

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows/:id`

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

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string |             |

{% tabs %}
{% tab title="200 Flow successfully retrieved" %}

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

{% endtab %}
{% endtabs %}

## Get Flows Count

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows/count`

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

{% tabs %}
{% tab title="200 " %}

```javascript
{
    "count": 29
}    
```

{% endtab %}
{% endtabs %}

## Create Flow

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows`

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

#### Request Body

| Name         | Type   | Description                                                                                                                                        |
| ------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| name         | string | Name of the flow.                                                                                                                                  |
| customFields | object | An object with any custom properties. This is useful for storing any custom metadata and later using the metadata values to filter returned flows. |
| thumbnail    | string | Flow thumbnail image.                                                                                                                              |
| flow         | object | Flow descriptor.                                                                                                                                   |
| wizard       | object | Wizard configuration for Integration Templates. See [Flow Wizard](#flow-wizard) for the full schema.                                               |
| notes        | object | Note blocks displayed in the Designer canvas. See [Flow Notes](#flow-notes) for the full schema.                                                   |

{% tabs %}
{% tab title="200 Flow successfully created." %}

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

{% endtab %}
{% endtabs %}

## Update Flow

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows/:id`

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

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string | Flow ID.    |

#### Query Parameters

<table><thead><tr><th width="201">Name</th><th width="147">Type</th><th>Description</th></tr></thead><tbody><tr><td>forceUpdate</td><td>boolean</td><td>A running flow cannot be updated unless forceUpdate=true.</td></tr></tbody></table>

#### Request Body

| Name | Type   | Description                                                                                                                                                                                      |
| ---- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|      | object | An object with `flow`, `name`, `customFields`, `thumbnail`, `sharedWith`, `wizard`, and `notes` parameters. `flow` is the Flow descriptor. See [Flow Notes](#flow-notes) for the `notes` schema. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Delete Flow

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows/:id`

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

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string | Flow ID.    |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Clone Flow

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows/:id/clone`

Clone a flow

#### Path Parameters

| Name                                 | Type   | Description |
| ------------------------------------ | ------ | ----------- |
| id<mark style="color:red;">\*</mark> | String | Flow ID     |

#### Request Body

| 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  | <p>Properties to be filtered from the flow model.</p><p>Example: "-thumbnail,-sharedWith". With this projection string, the thumbnail and sharedWith property will not be cloned.</p>                                                                                                     |
| 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.                                                                                                                                                                                                                   |

{% tabs %}
{% tab title="200: OK " %}

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

{% endtab %}
{% endtabs %}

## Flow Drafts

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.

### Overview

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

### Create a Draft

<mark style="color:green;">`POST`</mark> `http://[API-URL]/flows/:id/clone`

Create a draft by cloning an existing flow with special draft parameters.

#### Path Parameters

| Name                                 | Type   | Description                  |
| ------------------------------------ | ------ | ---------------------------- |
| id<mark style="color:red;">\*</mark> | String | Flow ID to create draft from |

#### Request Body

| Name                                                | Type    | Description                                                                             |
| --------------------------------------------------- | ------- | --------------------------------------------------------------------------------------- |
| setOriginFlowId<mark style="color:red;">\*</mark>   | Boolean | Must be `true` to link draft to original flow                                           |
| setComponentIdMap<mark style="color:red;">\*</mark> | Boolean | Must be `true` to map component IDs for publishing                                      |
| connectAccounts<mark style="color:red;">\*</mark>   | Boolean | Recommended `true` to copy account connections                                          |
| additional<mark style="color:red;">\*</mark>        | Object  | Must include `type` field set to `"automation-draft"` or `"integration-instance-draft"` |

```bash
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"
    }
  }'
```

{% tabs %}
{% tab title="200: OK Draft created successfully" %}

```javascript
{
  "cloneId": "draft-flow-id"
}
```

{% endtab %}

{% tab title="200: OK Existing draft returned" %}

```javascript
{
  "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.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
**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.
{% endhint %}

### Publish a Draft

<mark style="color:green;">`POST`</mark> `http://[API-URL]/drafts/:draftFlowId/publish`

Publish a draft to update the original flow with the draft's changes.

#### Path Parameters

| Name                                          | Type   | Description              |
| --------------------------------------------- | ------ | ------------------------ |
| draftFlowId<mark style="color:red;">\*</mark> | String | Draft flow ID to publish |

```bash
curl -XPOST "http://[API-URL]/drafts/draft-flow-id/publish" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK Draft published successfully" %}

```javascript
{
  "success": true,
  "flowId": "original-flow-id"
}
```

{% endtab %}

{% tab title="403: Forbidden User does not own the draft" %}

```javascript
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "You do not have permission to publish this draft."
}
```

{% endtab %}

{% tab title="404: Not Found Draft not found or not a draft type" %}

```javascript
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Draft not found or invalid draft type."
}
```

{% endtab %}
{% endtabs %}

### Publishing Process

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

{% hint style="warning" %}
**Publishing Impact**

Publishing a draft will immediately update the original flow. If the original flow is running, the changes will take effect.
{% endhint %}

### Use Cases

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

## Start/Stop Flow

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows/:id/coordinator`

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

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string | Flow ID.    |

#### Query Parameters (only for the stop command)

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

#### Request Body

| Name    | Type   | Description                                                                          |
| ------- | ------ | ------------------------------------------------------------------------------------ |
| command | string | The command to send to the flow coordinator. It can be either `"start"` or `"stop"`. |

{% tabs %}
{% tab title="200 " %}

```json
{
    "flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b",
    // equals the flowId if the background=true is sent in the query
    "ticket": "26544d8c-5209-44ac-9bdf-ef786924b07b"  
}
```

{% endtab %}
{% endtabs %}

## Get the status of the stop flow command

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/flows/{flowId}/coordinator/status`

{% tabs %}
{% tab title="200 " %}

```json
{
    "status": "completed",
    "stepsTotal":1
}
```

{% endtab %}
{% endtabs %}

## Send GET request to a component

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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". |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{
    // Response
}
```

{% endtab %}

{% tab title="202: Accepted If the component needs quota to process the webhook and it cannot get quota, Appmixer will return 202 and enqueue the incoming request for processing. It will be processed when quota is available." %}

```javascript
{
    // Response
}
```

{% endtab %}
{% endtabs %}

## Send POST request to a component

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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". |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{
    // Response
}
```

{% endtab %}

{% tab title="202: Accepted If the component needs quota to process the webhook and it cannot get quota, Appmixer will return 202 and enqueue the incoming request for processing. It will be processed when quota is available." %}

```javascript
{
    // Response
}
```

{% endtab %}
{% endtabs %}

## Send PUT request to a component

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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". |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{
    // Response
}
```

{% endtab %}

{% tab title="202: Accepted If the component needs quota to process the webhook and it cannot get quota, Appmixer will return 202 and enqueue the incoming request for processing. It will be processed when quota is available." %}

```javascript
{
    // Response
}
```

{% endtab %}
{% endtabs %}

## Flow Notes

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](#create-flow) and [Update Flow](#update-flow) endpoints.

### The `notes` object

`notes` is a map of **note ID → note object**. Each key is a UUID that uniquely identifies the note block.

```json
{
  "notes": {
    "ec488c75-b240-4b8a-8939-e05ba4af32a4": {
      "x": -368,
      "y": -128,
      "width": 432,
      "height": 288,
      "content": "## My note\n\nSupports **Markdown** content."
    }
  }
}
```

### Note object properties

| Property  | Type   | Description                                                                                                                                                   |
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `x`       | number | Horizontal position of the note block on the canvas (pixels, can be negative).                                                                                |
| `y`       | number | 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. |

## Flow Wizard

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`](/appmixer-ui-sdk/ui-and-widgets/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](#create-flow) and [Update Flow](#update-flow) endpoints.

### The `wizard` object

```json
{
  "wizard": {
    "fields": []
  }
}
```

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

### Field reference

Every field object shares a common set of properties:

| Property      | Type   | Description                                                                                       |
| ------------- | ------ | ------------------------------------------------------------------------------------------------- |
| `type`        | string | Field type. See the field types below.                                                            |
| `label`       | string | 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.                                                          |

#### `inspectorField`

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

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

#### `account`

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\[] | 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. |

```json
{
  "type": "account",
  "label": "Slack account",
  "tooltip": "",
  "attrs": {
    "service": "appmixer:slack",
    "components": ["a0828f32"],
    "sharedAccountId": null
  },
  "options": {}
}
```

#### `customField`

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

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

#### `text`

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 | The text content to display.     |

```json
{
  "type": "text",
  "label": "",
  "tooltip": "",
  "attrs": {
    "format": "header",
    "text": "Configure your Slack alarm"
  },
  "options": {}
}
```

#### `image`

A static image displayed inside the Wizard form.

| `attrs` property | Type   | Description        |
| ---------------- | ------ | ------------------ |
| `image`          | string | Image URL or path. |

```json
{
  "type": "image",
  "label": "",
  "tooltip": "",
  "attrs": {
    "image": "https://example.com/wizard-banner.png"
  },
  "options": {}
}
```

### Complete example

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.

```bash
curl -XPOST "https://api.YOUR_TENANT.appmixer.cloud/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": {}
        }
      ]
    }
  }'
```

### Filtering Integration Templates

An Integration Template is a flow that has a non-empty `wizard` property and no `templateId` property. To list all published templates:

```
GET /flows?filter=templateId:!&filter=wizard.fields
```

To list a user's integration instances (flows created from a template):

```
GET /flows?filter=templateId
```

{% hint style="info" %}
For a guided walkthrough of building and publishing templates using the no-code Studio, see [Build and Publish a Template](/getting-started/integrations).
{% endhint %}

### Send DELETE request to a component <a href="#send-delete-request-to-a-component" id="send-delete-request-to-a-component"></a>

<mark style="color:red;">`DELETE`</mark> `https://api.appmixer.com/flows/:flowId/components/:componentId`

**Query Parameters**

| Name        | Type    | Description                                                                                                                                                                                                                                                                          |
| ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| enqueueOnly | Boolean | <p>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</p><p>"false".</p> |


# Apps

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

## Get Apps

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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]"`

#### Query Parameters

| Name      | Type    | Description                                                                                          |
| --------- | ------- | ---------------------------------------------------------------------------------------------------- |
| ignoreACL | boolean | Admin only. When `true`, returns every app regardless of ACL. Non-admin users receive a `400` error. |

{% tabs %}
{% tab title="200 Apps successfully retrieved." %}

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

{% endtab %}
{% endtabs %}

## Get Bundles

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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]"`

{% tabs %}
{% tab title="200 " %}

```javascript
{
    "appmixer.asana": { "version": "1.2.0" },
    "appmixer.calendly": { "version": "2.0.1" },
    "appmixer.dropbox": { "version": "1.5.3" }
}
```

{% endtab %}
{% endtabs %}

## Get App Components

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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]"`

#### Query Parameters

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

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get All Components

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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]"`

#### Query Parameters

| 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  | Restrict the result to a subset of components. A `[vendor].[service].[module].[component]` selector (e.g. `appmixer.google` or `appmixer.google.gmail`). |
| 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.                                               |

{% tabs %}
{% tab title="200 (manifest=no)" %}

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

{% endtab %}

{% tab title="200 (default - full manifests)" %}

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

{% endtab %}
{% endtabs %}

## Download a Component/Module/Service

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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`

#### Path Parameters

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

{% tabs %}
{% tab title="200 " %}

```
(binary zip archive)
```

{% endtab %}

{% tab title="404 " %}

```
{
    "error": "There is nothing that corresponds to appmixer.myservice."
}
```

{% endtab %}
{% endtabs %}

## Publish A Component/Module/Service

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/components`

Publish a new component, new module or an entire service or update an existing component/module/service. The uploaded entity must be zipped and must have a proper directory/file structure inside (See Basic Component Structure for more details). Note that you can use the Appmixer CLI tool to pack your component/service/module to a zip archive (`appmixer pack` command). Alternatively, you can also use a regular zip command line utility but you should omit the node\_modules/ directories before archiving and the root directory of the zip archive must match the vendor name. The directory hierarchy must have a well defined structure: `[vendor]/[service]/[module]/[component]`.\
\
The size limit of the zip archive is 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`

#### Query Parameters

| Name       | Type    | Description                                                                                                                                    |
| ---------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| compile    | boolean | When `true`, the uploaded component code is compiled (minified) during publishing.                                                             |
| replaceAll | boolean | When `true`, existing files of the published service/module/component that are not present in the uploaded archive are removed (full replace). |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Check for Publishing Progress

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/components/uploader/:ticket`

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

#### Path Parameters

| Name   | Type   | Description                                           |
| ------ | ------ | ----------------------------------------------------- |
| ticket | string | Ticket that you got from the POST /component request. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}

{% tab title="404 " %}

```
{
    "error": "There's no upload under ticket 2e9dd726-2b7f-46f7-bea4-8db7f7175aa8"
}
```

{% endtab %}
{% endtabs %}

## Delete a Component/Module/Service

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/components/:selector`

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

#### Path Parameters

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

#### Query Parameters

| Name                   | Type    | Description                                                                                                            |
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
| doNotRemoveCollections | boolean | When `true`, the database collections associated with the deleted component(s) are preserved instead of being dropped. |

{% tabs %}
{% tab title="200 " %}

```
{}
```

{% endtab %}
{% endtabs %}


# Accounts

Authentication to apps.

## Get Accounts

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/auth/:componentType`

Get the list of accounts the user has previously authenticated with for this component type.\
\
`curl "https://api.acme.com/auth/appmixer.slack.list.SendChannelMessage?componentId=e15ef119-8fcb-459b-aaae-2a3f9ee41f15" -H "Authorization: Bearer [ACCESS_TOKEN]"`<br>

#### Path Parameters

| Name          | Type   | Description     |
| ------------- | ------ | --------------- |
| componentType | string | Component Type. |

#### Query Parameters

| Name        | Type   | Description   |
| ----------- | ------ | ------------- |
| componentId | string | Component ID. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get All Accounts

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/accounts`

Get the list of all accounts the user has authenticated with to any component.\
\
`curl "https://api.appmixer.com/accounts" -H "Authorization: Bearer [ACCESS_TOKEN]"`<br>

#### Query Parameters

| Name   | Type   | Description              |
| ------ | ------ | ------------------------ |
| filter | string | You can filter accounts. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

Example of filtering certain accounts:

```
// filtering acme accounts and aws accounts
curl --request GET 'http://api.acme.com/accounts?filter=service:!acme:[service]&filter=service:!appmixer:aws' \
--header 'Authorization: Bearer [ACCESS_TOKEN]'
```

## Update Account Info

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/accounts/:accountId`

Update account information. Currently, only the display name can be updated. The display name is visible in the Designer inspector when selecting available accounts for a component type and also on the Accounts page.\
\
`curl -XPUT "https://api.appmixer.com/accounts/5a6e21f3b266224186ac7d03" -H "Authorization: Bearer [ACCESS_TOKEN]" -H "Content-Type: application/json" -d '{ "displayName": "My Account Name" }'`

#### Path Parameters

| Name      | Type   | Description                      |
| --------- | ------ | -------------------------------- |
| accountId | string | The ID of the account to update. |

#### Request Body

| Name | Type   | Description                         |
| ---- | ------ | ----------------------------------- |
|      | string | Human-readable name of the account. |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}

## Create Account

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/accounts`

This can be used to create an account. Usually, an account is created when the user authenticates a component. There are scenarios where it is beneficial to create an account without user interaction (Integrations). There has to be an authentication module (auth.js) installed in Appmixer corresponding to the \`service\` ID. All the built-in authentication types are supported (Oauth1, Oauth2, API Key).

#### Query Parameters

| Name               | Type   | Description                                                                                                        |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------ |
| validateScope      | string | If `false`, then the scope of the token from the body won't be validated against components installed in Appmixer. |
| requestProfileInfo | string | If `false`, then the auth module requestProfileInfo function won't be called.                                      |

#### Request Body

| Name        | Type   | Description                                                                                                                     |
| ----------- | ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| displayName | string | Display name property of the account. This overrides the name of the account in the frontend.                                   |
| name        | string | Name of the account, the authentication will determine the name of the account using the `accountNameFromProfileInfo` property. |
| service     | string | ID (vendor:service) of the service - \`appmixer:google\` for example.                                                           |
| token       | object | <p>The structure of this object depends on the authentication type (Oauth1, Oauth2, API Key).<br></p>                           |
| profileInfo | object | Can be provided directly. If not, `requestProfileInfo` from the authentication module will be called.                           |

{% tabs %}
{% tab title="200 " %}

```
{
    "accountId": "5f841f3a43f477a9fa8fa4e9",
    "name": "[Name of the account]",
    "displayName": null,
    "service": "[vendor:service]",
    "userId": "5f804b96ea48ec47a8c444a7",
    "profileInfo": {
        
    },
    "pre": {},
    "revoked": false
}
```

{% endtab %}
{% endtabs %}

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

```
curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
    "service": "appmixer:slack",
    "token": {
        "accessToken": "[slack access token]",
        "scope": [
            "channels:write", 
            "groups:write", 
            "channels:read", 
            "channels:history", 
            "groups:read", 
            "groups:history", 
            "users:read", 
            "chat:write:user"
        ]
    },
    "profileInfo": {
        "id" : "[Name of the account that will be used in the frontend]"
    }
}'
```

The `profileInfo` object is optional. If you provide it it will be used. If you do not provide it then the [`requestProfileInfo`](/building-connectors/authentication#requestprofileinfo-function-object-or-string-optional) 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:

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

An example, this time an API Key account:

```
curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
    "service": "appmixer:aws",
    "token": {
        "accessKeyId" : "[AWS access key ID]",
        "secretKey" : "[AWS secret key]"
    }
}'
```

Another example with a PWD account type:

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

## Test Account

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/accounts/:accountId/test`

Test account. Check if all the credentials (tokens) are still valid for the account.\
\
`curl -XPOST "https://api.appmixer.com/accounts/5a6e21f3b266224186ac7d03/test" -H "Authorization: Bearer [ACCESS_TOKEN]"`

#### Path Parameters

| Name      | Type   | Description |
| --------- | ------ | ----------- |
| accountId | string | Account ID. |

{% tabs %}
{% tab title="200 " %}

```javascript
{ "5a6e21f3b266224186ac7d04": "valid" }
```

{% endtab %}
{% endtabs %}

## Remove Account

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/accounts/:accountId`

Remove the account and stop all the flows that this account is used in.\
\
`curl -XDELETE "https://api.appmixer.com/accounts/5a6e21f3b266224186ac7d03" -H "Authorization: Bearer [ACCESS_TOKEN]"`

#### Path Parameters

| Name      | Type   | Description |
| --------- | ------ | ----------- |
| accountId | string | Account ID. |

{% tabs %}
{% tab title="200 " %}

```javascript
{ "accountId": "5abcd0ddc4c335326198c1b2" }
```

{% endtab %}
{% endtabs %}

## List All Flows Using Account

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/accounts/:accountId/flows`

List all the flows where the account is used.\
\
`curl "https://api.appmixer.com/accounts/5a6e21f3b266224186ac7d03/flows" -H "Authorization: Bearer [ACCESS_TOKEN]"`

#### Path Parameters

| Name      | Type   | Description |
| --------- | ------ | ----------- |
| accountId | string | Account ID. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Generate Authentication Session Ticket

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/auth/ticket`

Generate an authentication session ticket. This is the first call to be made before the user can authentication to a service. The flow is as follows:\
\
1\. Generate an authentication session ticket.\
2\. Get an authentication URL.\
3\. Start an authentication session.\
4\. Open the authentication URL in a browser to start the authentication flow.\
5\. Once the user completes the authentication flow, the browser redirects the user to a special Appmixer page which posts a message of the form "appmixer.auth.\[success/failure].\[ticket]" via the window\.postMessage() call: <https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage.\\>
\
Note that this is a low-level mechanism that you don't have to normally deal with. The Appmixer JS SDK handles all this for you.\
\
`curl "https://api.appmixer.com/auth/ticket" -H "Authorization: Bearer [ACCESS_TOKEN]"`

{% tabs %}
{% tab title="200 " %}

```javascript
{ "ticket": "58593f07c3ee4f239dc69ff7:1d2a90df-b192-4a47-aaff-5a80bab66de5" }
```

{% endtab %}
{% endtabs %}

## Get Authentication URL

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/auth/:componentType/auth-url/:ticket`

Get an authentication URL.\
\
`curl "https://api.appmixer.com/auth/appmixer.slack.list.SendChannelMessage/auth-url/58593f07c3ee4f239dc69ff7:1d2a90df-b192-4a47-aaff-5a80bab66de5" -H "Authorization: Bearer [ACCESS_TOKEN]"`

#### Path Parameters

| Name          | Type   | Description            |
| ------------- | ------ | ---------------------- |
| ticket        | string | Authentication ticket. |
| componentType | string | Component type.        |

#### Query Parameters

| Name | Type   | Description   |
| ---- | ------ | ------------- |
|      | string | Component ID. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get Authentication Status

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/auth/status/:ticket`

{% tabs %}
{% tab title="200: OK " %}

```json
{
    "accountId": "5bc0bad6f4cb78001167b173",
    "tokenId": "65c49d44e49f774bb587c4e1",
    "finished": true,
    "updatedAt": "2024-02-08T09:22:12.496Z",
    "error": null
}
```

{% endtab %}
{% endtabs %}

## Clear Authentication From Component

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/auth/component/:componentId`

Clear authentication associated with the component. Note that this call does not remove the account, it only removes the association of an account with a component.\
\
`curl -XDELETE "https://api.appmixer.com/auth/component/e25dc901-f92a-46a2-8d29-2573d4ad65e5" -H "Authorization: Bearer [ACCESS_TOKEN]"`

#### Path Parameters

| Name        | Type   | Description   |
| ----------- | ------ | ------------- |
| componentId | string | Component ID. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Assign Account To Component

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/auth/component/:componentId/:accountId`

Assign an account to a component.\
\
`curl -XPUT "https://api.appmixer.com/auth/component/e25dc901-f92a-46a2-8d29-2573d4ad65e5/5a6e21f3b266224186ac7d03" -H "Authorization: Bearer [ACCESS_TOKEN]"`

#### Path Parameters

| Name        | Type   | Description   |
| ----------- | ------ | ------------- |
| accountId   | string | Account ID.   |
| componentId | string | Component ID. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Share Account

<mark style="color:green;">`POST`</mark> `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.

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

#### Path Parameters

| Name      | Type   | Description                |
| --------- | ------ | -------------------------- |
| accountId | string | ID of the account to share |

#### Request Body

| Name                                           | Type   | Description                                                |
| ---------------------------------------------- | ------ | ---------------------------------------------------------- |
| flowId<mark style="color:red;">\*</mark>       | string | ID of the integration template to share the account with   |
| componentIds<mark style="color:red;">\*</mark> | array  | Array of component IDs that should use this shared account |

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "accountId": "5a6e21f3b266224186ac7d03",
  "flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
  "shared": [
    {
      "componentId": "component-1",
      "componentType": "appmixer.gmail.SendEmail"
    },
    {
      "componentId": "component-2",
      "componentType": "appmixer.slack.SendMessage"
    }
  ]
}
```

{% endtab %}

{% tab title="400: Bad Request" %}

```javascript
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "No valid tokens found for componentType: appmixer.gmail.SendEmail"
}
```

The account doesn't have valid tokens with the required scope for the component type.
{% endtab %}

{% tab title="403: Forbidden" %}

```javascript
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```

You don't own this account and cannot share it.
{% endtab %}

{% tab title="404: Not Found" %}

```javascript
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Flow 9089f275-f5a5-4796-ba23-365412c5666e not found"
}
```

The specified flow (integration template) doesn't exist.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
**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.
{% endhint %}

{% hint style="warning" %}
**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
  {% endhint %}

### Use Cases

**Shared Database Access**

```bash
# 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"]
  }'
```

**API Rate Limit Management**

```bash
# 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"]
  }'
```

## Unshare Account

<mark style="color:green;">`POST`</mark> `http://[API-URL]/accounts/:accountId/unshare`

Remove account sharing from an integration template. This immediately revokes access for all instances created from the template.

```bash
curl -XPOST "http://[API-URL]/accounts/5a6e21f3b266224186ac7d03/unshare" \
  -H "Authorization: Bearer [ACCESS_TOKEN]" \
  -H "Content-type: application/json" \
  -d '{
    "flowId": "integration-template-id"
  }'
```

#### Path Parameters

| Name      | Type   | Description                  |
| --------- | ------ | ---------------------------- |
| accountId | string | ID of the account to unshare |

#### Request Body

| Name                                     | Type   | Description                                    |
| ---------------------------------------- | ------ | ---------------------------------------------- |
| flowId<mark style="color:red;">\*</mark> | string | ID of the integration template to unshare from |

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "accountId": "5a6e21f3b266224186ac7d03",
  "flowId": "9089f275-f5a5-4796-ba23-365412c5666e"
}
```

{% endtab %}

{% tab title="403: Forbidden" %}

```javascript
{
  "statusCode": 403,
  "error": "Forbidden",
  "message": "Insufficient permissions"
}
```

You don't own this account and cannot unshare it.
{% endtab %}

{% tab title="404: Not Found" %}

```javascript
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Flow 9089f275-f5a5-4796-ba23-365412c5666e not found"
}
```

The specified flow doesn't exist.
{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**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.
{% endhint %}


# User

API for users

## Sign-in User

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user/auth`

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

#### Request Body

| Name                                       | Type   | Description                            |
| ------------------------------------------ | ------ | -------------------------------------- |
| password<mark style="color:red;">\*</mark> | string | Password.                              |
| username<mark style="color:red;">\*</mark> | string | Username, has to have an email format. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Create User

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user`

Create user. By default, this endpoint is open (does not require authentication). This can be changed by setting the API\_USER\_CREATE\_SCOPE [system configuration](/appmixer-backoffice/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" }'`

#### Request Body

| Name                                       | Type   | Description        |
| ------------------------------------------ | ------ | ------------------ |
| password<mark style="color:red;">\*</mark> | string | Password.          |
| email                                      | string | Email address.     |
| username                                   | string | Email address.     |
| metadata                                   | object | Optional metadata. |

{% tabs %}
{% tab title="201: Created " %}

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

{% endtab %}
{% endtabs %}

## Get Current User Information

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user`

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

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get User Information

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/users/:userId`

Admin token required.

## Get all users

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/users`

Admin token required.

Examples:

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

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

Get all users whose usernames include a pattern:

curl -XGET "<https://api.appmixer.com/users?pattern=joe>" -H 'Authorization: Beader \[ADMIN\_TOKEN]'

## Get the number of users

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/users/count`

Admin token required

## Update user

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/users/:userId`

Admin token required.

#### Request Body

| Name     | Type          | Description          |
| -------- | ------------- | -------------------- |
| scope    | Array         | Array of scopes.     |
| vendor   | String\|Array | One or more vendors. |
| metadata | Object        | Optional metadata.   |
| email    | String        | Email address.       |

## Delete user

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/users/:userId`

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

{% tabs %}
{% tab title="200: OK Returns JSON with a ticket. Deleting user may take a long time, you can then use the ticket and poll for the result using the next API method. " %}

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

{% endtab %}
{% endtabs %}

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/users/:userId/delete-status/:ticket`

{% tabs %}
{% tab title="200: OK " %}

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

{% endtab %}
{% endtabs %}

## Change user password

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user/change-password`

User token required.

#### Request Body

| Name                                          | Type   | Description  |
| --------------------------------------------- | ------ | ------------ |
| oldPassword<mark style="color:red;">\*</mark> | String | Old password |
| newPassword<mark style="color:red;">\*</mark> | String | New password |

## Reset user password

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user/reset-password`

Admin token required.

#### Request Body

| Name                                       | Type   | Description        |
| ------------------------------------------ | ------ | ------------------ |
| email<mark style="color:red;">\*</mark>    | String | User email address |
| password<mark style="color:red;">\*</mark> | String | New password       |

## Forgot Password

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloudforgot-password`

See the[ Forgot Password Service](/appmixer-backoffice/system-configuration#forgot-password-service) configuration for more details.

#### Request Body

| Name                                    | Type   | Description   |
| --------------------------------------- | ------ | ------------- |
| email<mark style="color:red;">\*</mark> | String | Email address |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Reset forgotten password

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/forgot-password/reset`

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

#### Request Body

| Name                                       | Type   | Description                                                          |
| ------------------------------------------ | ------ | -------------------------------------------------------------------- |
| password<mark style="color:red;">\*</mark> | String | New password. The minimum length of the password is five characters. |
| code<mark style="color:red;">\*</mark>     | String | Code generated via forgot-password.                                  |

{% tabs %}
{% tab title="200 " %}

```
{}
```

{% endtab %}
{% endtabs %}


# 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

## Create User Group

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user-groups`

Create a new user group. Admin token required.

```bash
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" } }'
```

#### Request Body

| Name                                   | Type   | Description                     |
| -------------------------------------- | ------ | ------------------------------- |
| name<mark style="color:red;">\*</mark> | string | Name of the user group          |
| metadata                               | object | Optional metadata for the group |

{% tabs %}
{% tab title="201: Created" %}

```javascript
{
  "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
  "name": "Marketing Team",
  "userId": "group-1678901234567",
  "metadata": {
    "department": "marketing"
  },
  "created": 1678901234567,
  "members": []
}
```

{% endtab %}
{% endtabs %}

## List User Groups

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user-groups`

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

```bash
curl -XGET "https://api.appmixer.com/user-groups" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

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

{% endtab %}
{% endtabs %}

## Get User Group

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user-groups/:groupId`

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

```bash
curl -XGET "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
  "name": "Marketing Team",
  "userId": "group-1678901234567",
  "metadata": {
    "department": "marketing"
  },
  "created": 1678901234567,
  "members": ["user1", "user2", "user3"]
}
```

{% endtab %}
{% endtabs %}

## Update User Group

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user-groups/:groupId`

Update user group details. Admin token required.

```bash
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" } }'
```

#### Request Body

| Name     | Type   | Description                          |
| -------- | ------ | ------------------------------------ |
| name     | string | Updated name of the group            |
| metadata | object | Updated metadata (replaces existing) |

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
  "name": "Marketing & Sales Team",
  "userId": "group-1678901234567",
  "metadata": {
    "department": "marketing",
    "region": "EMEA"
  },
  "created": 1678901234567
}
```

{% endtab %}
{% endtabs %}

## Delete User Group

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user-groups/:groupId`

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

```bash
curl -XDELETE "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e" \
  -H "Authorization: Bearer [ADMIN_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "success": true
}
```

{% endtab %}
{% endtabs %}

## Add Group Members

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user-groups/:groupId/members`

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

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

#### Request Body

| Name                                      | Type      | Description              |
| ----------------------------------------- | --------- | ------------------------ |
| userIds<mark style="color:red;">\*</mark> | string\[] | Array of user IDs to add |

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "added": ["user1", "user2", "user3"],
  "group": {
    "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
    "name": "Marketing Team",
    "members": ["user1", "user2", "user3"]
  }
}
```

{% endtab %}
{% endtabs %}

## Remove Group Member

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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.

```bash
curl -XDELETE "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e/members/user1" \
  -H "Authorization: Bearer [ADMIN_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "success": true,
  "removedUserId": "user1",
  "revokedTokens": 3
}
```

{% endtab %}
{% endtabs %}

## Get Group Members

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/user-groups/:groupId/members`

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

```bash
curl -XGET "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e/members" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "members": [
    {
      "id": "user1",
      "username": "john@example.com",
      "email": "john@example.com"
    },
    {
      "id": "user2",
      "username": "jane@example.com",
      "email": "jane@example.com"
    }
  ]
}
```

{% endtab %}
{% endtabs %}

## List User's Groups

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/users/:userId/groups`

Get all groups that a specific user belongs to.

```bash
curl -XGET "https://api.appmixer.com/users/user1/groups" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "groups": [
    {
      "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
      "name": "Marketing Team",
      "userId": "group-1678901234567",
      "metadata": {
        "department": "marketing"
      }
    },
    {
      "id": "5f8a7b2c3d4e5f6a7b8c9d0f",
      "name": "Admin Group",
      "userId": "group-1678901234568",
      "metadata": {}
    }
  ]
}
```

{% endtab %}
{% endtabs %}

## Switch Context

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/auth/switch-context`

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

```bash
curl -XPOST "https://api.appmixer.com/auth/switch-context" \
  -H "Authorization: Bearer [ACCESS_TOKEN]" \
  -H "Content-type: application/json" \
  -d '{ "groupId": "5f8a7b2c3d4e5f6a7b8c9d0e" }'
```

#### Request Body

| Name    | Type   | Description                                                       |
| ------- | ------ | ----------------------------------------------------------------- |
| groupId | string | Group ID to switch to, or null/omit to switch to personal context |

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "context": {
    "type": "group",
    "groupId": "5f8a7b2c3d4e5f6a7b8c9d0e",
    "groupName": "Marketing Team",
    "originalUserId": "user1"
  },
  "user": {
    "id": "group-1678901234567",
    "username": "group-1678901234567",
    "scope": ["user"],
    "type": "group"
  }
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**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.
{% endhint %}

## Get Available Contexts

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/auth/available-contexts`

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

```bash
curl -XGET "https://api.appmixer.com/auth/available-contexts" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

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

{% endtab %}
{% endtabs %}

## Use Cases

### Collaborative Teams

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
5. **Audit logs** maintain individual user accountability

### Admin Group

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

### SSO Integration

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

## Important Notes

{% hint style="warning" %}
**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.
{% endhint %}

{% hint style="info" %}
**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.
{% endhint %}

{% hint style="info" %}
**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
  {% endhint %}


# Insights

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

## Get Logs and Histogram

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/logs`

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

### Pagination with `searchAfter`

For efficient pagination through large result sets, use `searchAfter` instead of `from`. 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.

#### Query Parameters

| Name        | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ----------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| portType    | string | string: `in` or `out` . Or it can be array `portType=in&portType=out`. Used to filter only input messages or output messages. `in and out` by default.                                                                                                                                                                                                                                                                            |
| flowId      | string | The flow ID to filter on. This parameter can be used multiple times to filter on more flows. If not present, it will return logs for all user's flows (even flows that are being shared with signed in user).                                                                                                                                                                                                                     |
| 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](#pagination-with-searchafter) section above for details. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get Log Detail

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/log/:logIndex/:logId`

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

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

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

#### Path Parameters

| Name     | Type   | Description                                                                      |
| -------- | ------ | -------------------------------------------------------------------------------- |
| logId    | string | Log ID. Use the "\_id" property of the log object returned from flow logs.       |
| logIndex | string | Log index. Use the "\_index" property of the log object returned from flow logs. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get Logs (Aggregations)

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/logs`

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

#### Query Parameters

| Name          | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| flowId        | string | The flow ID to filter on. This parameter can be used multiple times to filter on more flows.                                                                                                                                                                                                                                                                                                                                      |
| 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 | <p>Query string. Uses the Lucene query syntax: <https://lucene.apache.org/core/2_9_4/queryparsersyntax.html><br></p>                                                                                                                                                                                                                                                                                                              |
| 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](#pagination-with-searchafter) section above for details. |

#### Request Body

| Name | Type   | Description                                                                                                                                                                                           |
| ---- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| aggs | object | <p>An object describing the desired aggregations. Uses Elasticsearch aggregation search structure: <https://elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html><br><br></p> |

{% tabs %}
{% tab title="200 This response is dynamic and its structure will depend on the body of the request as well. For example, aggregation names used on the request will be used in the response as well. It is also possible to query nested aggregations, leading to a more complex response as well." %}

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

{% endtab %}
{% endtabs %}

## Get Usage Information for Current User

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/telemetry`

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

#### Query Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| to   | string | To date.    |
| from | string | From date.  |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get Usage Information For Other Users

<mark style="color:blue;">`GET`</mark> `https://api.appmixer.com/telemetry/messages`

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

#### Query Parameters

| Name   | Type   | Description |
| ------ | ------ | ----------- |
| to     | string | To date.    |
| from   | string | From date.  |
| userId | string | A user ID.  |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get Flow Usage Information

<mark style="color:blue;">`GET`</mark> `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]"`

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}


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

<mark style="color:blue;">`GET`</mark> or <mark style="color:blue;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/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                                                                                   |
| --------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `_id`           | `string` | Elasticsearch document id                                                                        |
| `_index`        | `string` | Elasticsearch index name                                                                         |
| `es_index`      | `string` | Source index name                                                                                |
| `flowId`        | `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                                                                            |

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

## Variant 1: Component data event

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                                                      |
| --------------- | --------- | ------------------------------------------------------------------- |
| `_id`           | `string`  | Elasticsearch document id                                           |
| `_index`        | `string`  | 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                                                    |
| `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                                                     |

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

## Variant 2: Input queue data event

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                                   |
| --------------- | --------- | ------------------------------------------------ |
| `_id`           | `string`  | Elasticsearch document id                        |
| `_index`        | `string`  | 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                                |
| `timestamp`     | `string`  | ISO 8601 UTC datetime                            |
| `type`          | `string`  | Observed constant value: `data`                  |
| `userId`        | `string`  | User identifier                                  |

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

## Variant 3: Flow event

Observed as the flow-level event with `id = "flow"` and `type = "flow"`.

| Field             | Type      | Format / Notes                                    |
| ----------------- | --------- | ------------------------------------------------- |
| `_id`             | `string`  | Elasticsearch document id                         |
| `_index`          | `string`  | 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                                   |

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

## Variant 4: Component error event

Observed as a component-related error with `id = "component"`, `severity = "error"`, and without `type` in the sampled minimal form.

| Field           | Type     | Format / Notes                       |
| --------------- | -------- | ------------------------------------ |
| `_id`           | `string` | Elasticsearch document id            |
| `_index`        | `string` | 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                    |
| `timestamp`     | `string` | ISO 8601 UTC datetime                |
| `userId`        | `string` | User identifier                      |

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

## Variant 5: System retry error event

Observed as the system-level retry error with `id = "retry"` and `indexPrefix = "system"`.

| Field           | Type     | Format / Notes                    |
| --------------- | -------- | --------------------------------- |
| `_id`           | `string` | Elasticsearch document id         |
| `_index`        | `string` | Elasticsearch index name          |
| `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             |

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

## Minimal summary of observed discriminators

These are the constant-like fields that help identify the output variant:

| Variant                  | id            | indexPrefix | severity | type   | portType |
| ------------------------ | ------------- | ----------- | -------- | ------ | -------- |
| Component data event     | `component`   | `logstash`  | `info`   | `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`  | `—`    | `—`      |


# Files

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

## Get file info

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/files/metadata/:fileId`

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

#### Path Parameters

| Name   | Type   | Description                    |
| ------ | ------ | ------------------------------ |
| fileId | string | The UUID of the required file. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Upload a file

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/files`

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

#### Headers

| Name                  | Type   | Description                                                                                                                                                                                                                                                  |
| --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Content-type          | string | Must be set to `multipart/form-data`                                                                                                                                                                                                                         |
| uploader-file-id      | string | If set, the current file will be appended as a chunk to the file specified by this. If not present, a new file will be created. The response includes the resulting file's ID, so it can be used in subsequent requests to add chunks to the generated file. |
| uploader-file-name    | string | The name of the file. This will be ignored in case that `uploader-file-id header` is present.                                                                                                                                                                |
| uploader-chunk-size   | string | The size in bytes of the file/chunk being uploaded.                                                                                                                                                                                                          |
| uploader-chunk-number | string | This header is `uploader-chunk-number`. The ordinal number of this chunk - e.g. the first chunk is 1, the second is 2 and so on.                                                                                                                             |
| uploader-chunks-total | string | This header is `uploader-chunks-total`. The total number of chunks that compose the file. If set to 1, it means that this is the complete file                                                                                                               |

#### Request Body

| Name | Type   | Description                   |
| ---- | ------ | ----------------------------- |
| file | string | The file/chunk to be uploaded |

{% tabs %}
{% tab title="200 The fileId inside the response can be used to upload more chunks for the same file, or in other endpoints to reference this file." %}

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

{% endtab %}
{% endtabs %}

## Get user files

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/files`

Example: <https://api.appmixer.com/[files?limit=10\\&filter=filename:\\~invoice>]\(<http://localhost:2200/files?limit=10\\&filter=filename:~invoice)\\&sort=filename:1>

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

#### Query Parameters

| Name                        | Type    | Description                                                                                                                                                                           |
| --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| limit                       | number  |                                                                                                                                                                                       |
| offset                      | number  |                                                                                                                                                                                       |
| sort                        | String  |                                                                                                                                                                                       |
| projection                  | String  |                                                                                                                                                                                       |
| 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  |                                                                                                                                                                                       |

{% tabs %}
{% tab title="200: OK " %}

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

{% endtab %}
{% endtabs %}

## Get number of files

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/files/count`

Used for paging.

#### Query Parameters

| Name                        | Type    | Description                                                                                                                                                                           |
| --------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| includeComponentSourceFiles | Boolean | By default, the endpoint returns only files (that are used in flows). Custom component source codes are stored as files as well, if the user owns some, they can be returned as well. |
| filter                      | String  |                                                                                                                                                                                       |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{
    count: 2
}
```

{% endtab %}
{% endtabs %}

## Remove file.

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/files/:fileId`

{% tabs %}
{% tab title="200: OK " %}

```javascript
{
    // Response
}
```

{% endtab %}
{% endtabs %}

## Delete all user files.

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/files`

Example: DELETE <https://api.appmixer.com/[files?filter=filename:\\~invoice>]\(<http://localhost:2200/files?limit=10\\&filter=filename:~invoice>) will delete all user's files with 'invoice' in the name.

#### Query Parameters

| Name   | Type   | Description                  |
| ------ | ------ | ---------------------------- |
| filter | String | Can be used to filter files. |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{
    // Response
}
```

{% endtab %}
{% endtabs %}


# Data Stores

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

## Get All Stores

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/stores`

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

{% tabs %}
{% tab title="200 Cake successfully retrieved." %}

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

{% endtab %}
{% endtabs %}

## Get One Store metadata

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/stores/:id`

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

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string | Store ID.   |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get Number of Records in a Store

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/store/count`

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

#### Path Parameters

| Name    | Type   | Description |
| ------- | ------ | ----------- |
| storeId | string | Store ID.   |

{% tabs %}
{% tab title="200 " %}

```javascript
{
    "count": 681
}
```

{% endtab %}
{% endtabs %}

## Get Store Records

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/store`

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

#### Query Parameters

| Name    | Type   | Description                                                                                             |
| ------- | ------ | ------------------------------------------------------------------------------------------------------- |
| storeId | string | Store ID.                                                                                               |
| sort    | string | Store record parameter to sort by. Followed by ":" and the sort order -1 (descending) or 1 (ascending). |
| offset  | number | Index of the first record returned.                                                                     |
| limit   | number | Maximum number of records returned.                                                                     |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Create a new Store

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/stores`

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

#### Request Body

| Name | Type   | Description        |
| ---- | ------ | ------------------ |
| name | string | Name of the store. |

{% tabs %}
{% tab title="200 " %}

```json
{
    "storeId": "5c7f9bfe51dbaf0007f08db0"
}
```

{% endtab %}
{% endtabs %}

## Delete a Store

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/stores/:id`

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

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string | Store ID.   |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}

## Rename a Store

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/stores/:id`

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

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
| id   | string | Store ID.   |

#### Request Body

| Name | Type   | Description            |
| ---- | ------ | ---------------------- |
| name | string | New name of the store. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Create a new Store Item

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/store/:id/:key`

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

#### Path Parameters

| Name | Type   | Description                                      |
| ---- | ------ | ------------------------------------------------ |
| key  | string | Key under which the posted value will be stored. |
| id   | string | Store ID.                                        |

#### Request Body

| Name | Type   | Description                   |
| ---- | ------ | ----------------------------- |
|      | string | Value to store under the key. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Update key or value of an existing store item

<mark style="color:purple;">`PATCH`</mark> `https://api.YOUR_TENANT.appmixer.cloud/store/:id/:key`

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

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

#### Path Parameters

| Name                                  | Type   | Description                              |
| ------------------------------------- | ------ | ---------------------------------------- |
| id<mark style="color:red;">\*</mark>  | String | Store ID                                 |
| key<mark style="color:red;">\*</mark> | String | Key under which the updates are required |

#### Request Body

| Name  | Type   | Description |
| ----- | ------ | ----------- |
| key   | String | New key     |
| value | String | New Value   |

{% tabs %}
{% tab title="200: OK " %}

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

{% endtab %}
{% endtabs %}

## Delete Store Items

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/store`

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

#### Request Body

| Name  | Type  | Description                                                                    |
| ----- | ----- | ------------------------------------------------------------------------------ |
| items | array | Array of items to delete. Each item is an object of the form { key, storeId }. |

{% tabs %}
{% tab title="200 " %}

```javascript
{
    "deletedCount": 1
}
```

{% endtab %}
{% endtabs %}

## Download the content of a Data Store

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/store/download/:storeId`

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

#### Query Parameters

| Name   | Type   | Description                                      |
| ------ | ------ | ------------------------------------------------ |
| format | String | Default value 'json'. The other option is 'csv'. |

{% tabs %}
{% tab title="200: OK Content-Type: application/octet-stream, Content-Disposition" %}

```javascript
{
    // Response
}
```

{% endtab %}
{% endtabs %}


# Connector Configuration

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

{% hint style="warning" %}
Only users with admin scope can use these endpoints.
{% endhint %}

## Get Services Configuration

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/service-config`

Get a list of stored configurations.

#### Query Parameters

| Name    | Type   | Description                                                                                                |
| ------- | ------ | ---------------------------------------------------------------------------------------------------------- |
| pattern | string | A term to filter configurations containing pattern on their service id                                     |
| sort    | string | Sorting parameter. Service id can be used to sort results alphabetically by their id. Example: serviceId:1 |
| offset  | number | Index of the first item returned. Default is 0. Useful for paging.                                         |
| limit   | number | Maximum items returned. Default is 100. Useful for paging.                                                 |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get Service Configuration

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/service-config/:serviceId`

Get the configuration stored for the given service.

#### Path Parameters

| Name | Type   | Description                              |
| ---- | ------ | ---------------------------------------- |
|      | string | The service id. Example: appmixer:google |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Create Service Configuration

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/service-config`

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

#### Request Body

| Name      | Type   | Description                                                                        |
| --------- | ------ | ---------------------------------------------------------------------------------- |
| whatever  | string | Any value for the whatever-key                                                     |
| serviceId | string | The serviceId. It should be in the form `vendor:service`. Example: appmixer:google |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Update Service Configuration

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/service-config/:serviceId`

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

#### Path Parameters

| Name      | Type   | Description             |
| --------- | ------ | ----------------------- |
| serviceId | string | The service id. Example |

#### Request Body

| Name         | Type   | Description        |
| ------------ | ------ | ------------------ |
| whatever-key | string | Any value you need |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Delete Service Configuration

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/service-config/:serviceId`

Removes the configuration from the given service.

#### Path Parameters

| Name      | Type   | Description                              |
| --------- | ------ | ---------------------------------------- |
| serviceId | string | The service id. Example: appmixer:google |

{% tabs %}
{% tab title="200 " %}

```
{}
```

{% endtab %}
{% endtabs %}


# 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

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/acl-types`

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

{% tabs %}
{% tab title="200 Currently there are ACL rules for components and routes. In the future, other types might be added." %}

```javascript
[
    "routes",
    "components"
]
```

{% endtab %}
{% endtabs %}

## Get ACL rules for components|routes

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/acl/:type`

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

#### Path Parameters

| Name | Type   | Description          |
| ---- | ------ | -------------------- |
| type | string | components \| routes |

{% tabs %}
{% tab title="200 This is default list of ACL rules for components. All users can access all components." %}

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

{% endtab %}
{% endtabs %}

## Update ACL rules

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/acl/:type`

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

#### Path Parameters

| Name | Type   | Description          |
| ---- | ------ | -------------------- |
| type | string | components \| routes |

#### Request Body

| Name | Type  | Description                                                                                                            |
| ---- | ----- | ---------------------------------------------------------------------------------------------------------------------- |
|      | array | <p>Body has to be an array of ACL rules, where each rule has the following structure:<br>{<br>role: string - admin</p> |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}

## Get available resource values

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/acl/:type/resources`

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

#### Path Parameters

| Name | Type   | Description          |
| ---- | ------ | -------------------- |
| type | string | components \| routes |

{% tabs %}
{% tab title="200 Available options for routes type. In case of components it returns pattern that a resource has to match." %}

```
['*', 'flows']
```

{% endtab %}
{% endtabs %}

## Get available action values

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/acl/:type/actions`

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

#### Path Parameters

| Name | Type   | Description          |
| ---- | ------ | -------------------- |
| type | string | components \| routes |

{% tabs %}
{% tab title="200 Available options for routes type." %}

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

{% endtab %}
{% endtabs %}

## Get available options for attributes property.

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/acl/:type/resource/:resource/attributes`

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

#### Path Parameters

| Name     | Type   | Description                                            |
| -------- | ------ | ------------------------------------------------------ |
| type     | string | components \| routes                                   |
| resource | string | resource name - flows, appmixer.utils.controls.\*, ... |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}


# Charts

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

## Create Chart

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/charts`

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

#### Request Body

| Name    | Type   | Description                                                                                      |
| ------- | ------ | ------------------------------------------------------------------------------------------------ |
| traces  | object | The aggregations that are represented on the chart along with their sources (flows, components). |
| query   | object | Object representing time range for the chart.                                                    |
| options | object | Object with the visualization options for the chart.                                             |
| index   | number | The position of the chart in the dashboard.                                                      |
| type    | string | Type of the chart. `bar, line, scatter, area, pie`                                               |
| name    | string | Name of the chart.                                                                               |

{% tabs %}
{% tab title="200 " %}

```
{
    chartId: '5defb3901f17d98d974fbb00'
}
```

{% endtab %}
{% endtabs %}

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

## Update Chart

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/charts/:chartId`

The same properties as in Create Chart API endpoint.

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
|      | string |             |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get Charts

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/charts`

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

#### Query Parameters

| Name       | Type   | Description                                                                                                                          |
| ---------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| pattern    | string | Regex that will be used to match `name` property.                                                                                    |
| limit      | number | Maximum items returned. Default is 100. Used for paging.                                                                             |
| offset     | number | The index of the first item returned. Default is 0. Used for for paging.                                                             |
| sort       | string | Sorting parameter. Can be any chart object property followed by semicolon and 1 (ascending) or -1 (descending). Example: "mtime:-1". |
| projection | string | Exclude chart object properties. Example: "-traces".                                                                                 |

{% tabs %}
{% tab title="200 Charts successfully retrieved." %}

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

{% endtab %}
{% endtabs %}

## Get One Chart

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/charts/:id`

#### Path Parameters

| Name | Type   | Description                |
| ---- | ------ | -------------------------- |
| id   | string | ID of the chart to return. |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Delete a Chart

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/charts/:id`

#### Path Parameters

| Name | Type   | Description    |
| ---- | ------ | -------------- |
| id   | string | ID of a chart. |

{% tabs %}
{% tab title="200 " %}

```
```

{% endtab %}
{% endtabs %}


# Config

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

## Get all configuration entries

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/config`

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

{% tabs %}
{% tab title="200: OK " %}

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

{% endtab %}
{% endtabs %}

## Create a configuration key/value pair

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/config`

#### Request Body

| Name                                    | Type   | Description         |
| --------------------------------------- | ------ | ------------------- |
| key<mark style="color:red;">\*</mark>   | String | Configuration key   |
| value<mark style="color:red;">\*</mark> | Any    | Configuration value |

{% tabs %}
{% tab title="200: OK " %}

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

{% endtab %}
{% endtabs %}

## Removes a configuration entry

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/config/:key`

#### Path Parameters

| Name                                  | Type   | Description                                |
| ------------------------------------- | ------ | ------------------------------------------ |
| key<mark style="color:red;">\*</mark> | String | The key of the configuration to be removed |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{ "ok": true }
```

{% endtab %}
{% endtabs %}


# 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

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/modifiers`

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

{% tabs %}
{% tab title="200 Retrieves two objects: categories and modifiers" %}

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

{% endtab %}
{% endtabs %}

## Edit Modifiers

<mark style="color:orange;">`PUT`</mark> `https://api.YOUR_TENANT.appmixer.cloud/modifiers`

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

#### Request Body

| Name       | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ---------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| categories | object | <p>The object containing available modifier categories. Each category is composed by the following properties:<br>- <strong>label</strong>: label to be shown in the Modifier Editor. It can be overridden by string customization.<br>- <strong>index</strong>: position on which this category is displayed in the Modifier Editor.</p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| modifiers  | object | <p>The object containing available modifiers. Each modifier is composed of the following properties:<br><strong>- label:</strong> label to be shown in the Modifier Editor. Can be overridden by string customization.<br><strong>- category:</strong> an array containing the categories which the modifier will appear under.<br><strong>- description:</strong> short description of the modifier. It will appear under modifier's label. Can be overridden by string customization.<br><strong>- arguments (optional):</strong> 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 <code>{</code><br><code>name: String,</code><br><code>type: String,</code><br><code>isHash: Boolean, Optional</code><br><code>}</code><br><strong>- returns (optional):</strong> an object with just one property type indicating the expected return type.<br><strong>- isBlock (optional):</strong> boolean. Indicates if this is a block modifier.<br><strong>- private (optional):</strong> boolean. Indicates if the modifier is private and therefore not available for users.<br><strong>- variableArguments (optional):</strong> 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.<br>- <strong>helperFn</strong>: 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 <code>arguments</code> array.</p> |

{% tabs %}
{% tab title="200 It returns the whole updated definition, like the GET endpoint." %}

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

{% endtab %}
{% endtabs %}

## Delete Modifiers

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/modifiers`

Delete all modifiers. Restricted to **admin** users only.

{% tabs %}
{% tab title="200 Empty response" %}

```
{}
```

{% endtab %}
{% endtabs %}

## Test Modifier Function

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/modifiers/test`

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

#### Request Body

| Name      | Type   | Description                                        |
| --------- | ------ | -------------------------------------------------- |
| helperFn  | string | The Javascript helper function as a string.        |
| arguments | array  | The arguments to be passed to the helper function. |

{% tabs %}
{% tab title="200 The endpoint will respond with the returned value from the helperFn." %}

```
4
```

{% endtab %}

{% tab title="400 The helper function must always return a value. In the case it does not, this response is received." %}

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

{% endtab %}
{% endtabs %}


# 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

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/public-files`

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

{% tabs %}
{% tab title="200: OK " %}

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

{% endtab %}
{% endtabs %}

## Upload a public file

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/public-files`

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

#### Request Body

| Name                                       | Type   | Description             |
| ------------------------------------------ | ------ | ----------------------- |
| filename<mark style="color:red;">\*</mark> | String | The name for the file   |
| file<mark style="color:red;">\*</mark>     | File   | The file to be uploaded |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{ "ok": true }
```

{% endtab %}
{% endtabs %}

## Removes a public file

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/public-files/:filename`

#### Path Parameters

| Name                                       | Type   | Description                             |
| ------------------------------------------ | ------ | --------------------------------------- |
| filename<mark style="color:red;">\*</mark> | String | The name of the file you want to remove |

{% tabs %}
{% tab title="200: OK " %}

```javascript
{ "ok": true }
```

{% endtab %}
{% endtabs %}


# 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

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/unprocessed-messages`

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

#### Path Parameters

| Name | Type   | Description |
| ---- | ------ | ----------- |
|      | string |             |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

## Get message

<mark style="color:blue;">`GET`</mark> `https://api.YOUR_TENANT.appmixer.cloud/unprocessed-messages/:messageId`

Get a single message.

#### Path Parameters

| Name      | Type   | Description |
| --------- | ------ | ----------- |
| messageId | string |             |

{% tabs %}
{% tab title="200 " %}

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

{% endtab %}
{% endtabs %}

<mark style="color:red;">`DELETE`</mark> `https://api.YOUR_TENANT.appmixer.cloud/unprocessed-messages/:messageId`

Delete a message.

#### Path Parameters

| Name      | Type   | Description |
| --------- | ------ | ----------- |
| messageId | string |             |

{% tabs %}
{% tab title="200 " %}

```
{}
```

{% endtab %}
{% endtabs %}

## Retry a message

<mark style="color:green;">`POST`</mark> `https://api.YOUR_TENANT.appmixer.cloud/unprocessed-messages/:messageId`

Put the message back into Appmixer engine.

#### Path Parameters

| Name      | Type   | Description |
| --------- | ------ | ----------- |
| messageId | string |             |

{% tabs %}
{% tab title="200 " %}

```
{}
```

{% endtab %}
{% endtabs %}


# Variables

## Get flow variables

<mark style="color:green;">`POST`</mark> `/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              |
| ------------- | ------------------ |
| Content-Type  | `application/json` |
| Authorization | `Bearer <token>`   |

**Body**

| Name                  | Type           | Description                                                                                                     |
| --------------------- | -------------- | --------------------------------------------------------------------------------------------------------------- |
| useCache              | boolean        | Default `true`.                                                                                                 |
| flow                  | boolean        | Default `true`. If `true`, the response object will contain flow variables, too.                                |
| components            | object         |                                                                                                                 |
| components.IDs        | array\<string> | Array of component IDs                                                                                          |
| components.links      | boolean        | Default `true`. If `true`, the response object will contain *input link* variables for the given component IDs. |
| components.properties | boolean        | Default `true`. If true, the response object will contains *properties* variables for the given component IDs.  |

**Response**

{% tabs %}
{% tab title="200" %}

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

{% endtab %}
{% endtabs %}

#### Example:

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


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

## Overview

* Categories are global/system-wide resources
* Admin-only access for create/update/delete operations
* All authenticated users can read categories
* Templates can belong to multiple categories
* Only integration templates and drafts can have categories
* Deleting a category removes it from all associated templates

## List Categories

<mark style="color:blue;">`GET`</mark> `http://[API-URL]/categories`

Get all categories. Available to all authenticated users.

```bash
curl -XGET "http://[API-URL]/categories" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

```javascript
[
  {
    "id": "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
  }
]
```

{% endtab %}
{% endtabs %}

## Get Category

<mark style="color:blue;">`GET`</mark> `http://[API-URL]/categories/:categoryId`

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

```bash
curl -XGET "http://[API-URL]/categories/5f8a7b2c3d4e5f6a7b8c9d0e" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

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

{% endtab %}

{% tab title="404: Not Found" %}

```javascript
null
```

{% endtab %}
{% endtabs %}

## Create Category

<mark style="color:green;">`POST`</mark> `http://[API-URL]/categories`

Create a new category. Admin token required.

```bash
curl -XPOST "http://[API-URL]/categories" \
  -H "Authorization: Bearer [ADMIN_TOKEN]" \
  -H "Content-type: application/json" \
  -d '{ "name": "CRM", "description": "Customer relationship management integrations" }'
```

#### Request Body

| Name                                   | Type   | Description                                      |
| -------------------------------------- | ------ | ------------------------------------------------ |
| name<mark style="color:red;">\*</mark> | string | Category name (1-100 characters), must be unique |
| description                            | string | Category description (max 500 characters)        |

{% tabs %}
{% tab title="201: Created" %}

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

{% endtab %}

{% tab title="400: Bad Request - Duplicate Name" %}

```javascript
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Category with this name already exists."
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
**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.
{% endhint %}

## Update Category

<mark style="color:orange;">`PUT`</mark> `http://[API-URL]/categories/:categoryId`

Update an existing category. Admin token required.

```bash
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" }'
```

#### Request Body

| Name        | Type   | Description                              |
| ----------- | ------ | ---------------------------------------- |
| name        | string | Updated category name (1-100 characters) |
| description | string | Updated description (max 500 characters) |

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "id": "5f8a7b2c3d4e5f6a7b8c9d0e",
  "name": "CRM & Sales",
  "description": "Customer relationship management and sales tools",
  "created": 1678901234567,
  "mtime": 1678905678901
}
```

{% endtab %}

{% tab title="400: Bad Request - Duplicate Name" %}

```javascript
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Category with this name already exists."
}
```

{% endtab %}

{% tab title="404: Not Found" %}

```javascript
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Category not found."
}
```

{% endtab %}
{% endtabs %}

## Delete Category

<mark style="color:red;">`DELETE`</mark> `http://[API-URL]/categories/:categoryId`

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

```bash
curl -XDELETE "http://[API-URL]/categories/5f8a7b2c3d4e5f6a7b8c9d0e" \
  -H "Authorization: Bearer [ADMIN_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

```javascript
{
  "success": true
}
```

{% endtab %}

{% tab title="404: Not Found" %}

```javascript
{
  "statusCode": 404,
  "error": "Not Found",
  "message": "Category not found."
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**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.
{% endhint %}

## Assigning Categories to Templates

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

### Create Flow with Categories

<mark style="color:green;">`POST`</mark> `http://[API-URL]/flows`

```bash
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": { ... }
  }'
```

### Update Flow Categories

<mark style="color:orange;">`PUT`</mark> `http://[API-URL]/flows/:flowId`

```bash
curl -XPUT "http://[API-URL]/flows/[FLOW_ID]" \
  -H "Authorization: Bearer [ACCESS_TOKEN]" \
  -H "Content-type: application/json" \
  -d '{
    "categories": ["5f8a7b2c3d4e5f6a7b8c9d0e"]
  }'
```

### Get Flow with Categories

<mark style="color:blue;">`GET`</mark> `http://[API-URL]/flows/:flowId`

```bash
curl -XGET "http://[API-URL]/flows/[FLOW_ID]" \
  -H "Authorization: Bearer [ACCESS_TOKEN]"
```

{% tabs %}
{% tab title="200: OK" %}

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

{% endtab %}
{% endtabs %}

## Category Restrictions

{% hint style="warning" %}
**Flow Type Restrictions**

Categories can only be assigned to:

* Integration templates (`integration-template`)
* 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.
{% endhint %}

## Category Validation

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

### Validation Example

```bash
# 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"]
  }'
```

{% tabs %}
{% tab title="400: Bad Request" %}

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

{% endtab %}
{% endtabs %}

## Use Cases

### Marketplace Organization

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

### Example Workflow

```bash
# 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
```

## Audit Logging

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](/appmixer-backoffice/audit-logs) documentation for more information.


# Flow

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

![Flow](/files/-L_wYg_3yos4MYamt4h9)

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

## Flow Descriptor

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

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

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


# 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](/files/-L_wYg_3yos4MYamt4h9)

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.

![Inspector panel](/files/-L_wYs9HncFTna2H0Lgz)

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.


# Basic Structure

## Introduction

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

![Services, modules and components hierarchy](/files/-M16hhlXmhHwfUzDjkby)

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

![Google modules](/files/-M17LYg5LQGI1VsoL1p_)

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

![Twilio service](/files/-M17MBk6aUwpEjzvOdvx)

## Directory Structure

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

![Modules as separate apps.](/files/-M16nF6sN3vHtMB9fJj7)

![A single app type of service.](/files/-M16nJVE1sUPQ3F2zFNu)

## Service Manifest File

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

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

Available fields are:

| Field           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | The name of the service. The name must have the `[vendor].[service]` format where `[vendor]` is the Vendor name (See e.g. [Enabling Users to Publish Custom Components ](/appmixer-self-managed/installation#enabling-users-to-publish-custom-components)for more details). Normally you'll have just one vendor or use the default `'appmixer'` vendor. `[service]` is the name of your service. Example: `"appmixer.google"`, `"appmixer.twilio"`, ... . |
| `label`         | The label of your app.                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `category`      | App category. By default, components shipped with Appmixer are divided into two categories "applications" and "utilities" but you can have your own custom categories too. Just use any custom category name in the service manifest file to create a new category and add your service to it. This category will become automatically visible e.g. in the Appmixer Designer UI.                                                                           |
| `categoryIndex` | App category index. By default, categories are sorted alphabetically, you can change that using this index property. Optional.                                                                                                                                                                                                                                                                                                                             |
| `index`         | The app index within the category. This allows sorting the apps within the same category.                                                                                                                                                                                                                                                                                                                                                                  |
| `description`   | Description of your app.                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `icon`          | App icon in the Data URI format.                                                                                                                                                                                                                                                                                                                                                                                                                           |

![Service manifest fields meaning.](/files/-M17R7AooNwoLvvxR_fI)

## Module Manifest File

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

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

Available fields are:

| Field           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | The name of the module. The name must have the `[vendor].[service].[module]` format where `[vendor]` is the Vendor name (See e.g. [Enabling Users to Publish Custom Components ](/appmixer-self-managed/installation#enabling-users-to-publish-custom-components)for more details). Normally you'll have just one vendor or use the default `'appmixer'` vendor. `[service]` is the name of your service and `[module]` is the name of your module. Examples: `"appmixer.google.gmail"`, `"appmixer.google.calendar"`, .... . Note that the directory structure of your module must follow this name. In other words, if you have a module named `"appmixer.myservice.mymodule"`, your directory structure will look like this: *myservice/mymodule*. |
| `label`         | The label of your app.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `category`      | App category. By default, components shipped with Appmixer are divided into two categories "applications" and "utilities" but you can have your own custom categories too. Just use any custom category name in the module manifest file to create a new category and add your app to it. This category will become automatically visible e.g. in the Appmixer Designer UI.                                                                                                                                                                                                                                                                                                                                                                           |
| `categoryIndex` | App category index. By default, categories are sorted alphabetically, you can change that using this index property. Optional.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `index`         | The app index within the category. This allows sorting the apps within the same category.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `description`   | Description of your app.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `icon`          | App icon in the Data URI format.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |


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


# name

(*required)*

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

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

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


# label

(*optional)*

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

```
{ "label": "Create Tweet" }
```


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


# description

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

![Component Description](/files/-L_wZRQ0CgS1eEtwPt6W)

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

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


# 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](/building-connectors/authentication) 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](/files/-L_wa-u3yA7oI5VjxcFw)


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

```json
{
    "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](/files/-L_wcC27OQsaMGPIBmg2)

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

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

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

## inPort.schema

Definition of the schema of the data that the input port expects. Please see the [Properties Schema](https://docs.appmixer.com/building-connectors/manifest/pages/-LATDlWTegDtvBmHEaZx#properties.schema) section for more details.

## inPort.inspector

Definition of the inspector UI for this input port. Please see the [Properties Inspector](https://docs.appmixer.com/building-connectors/manifest/pages/-LATDlWTegDtvBmHEaZx#properties.inspector) section for more details.

## inPort.source

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

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

```json
{
...
    "inPorts": [
        {
            "name": "in",
            "schema": {
                "type": "object"
            },
            "source": {
                // The ListColumns component can return an array of columns in a
                // Worksheet.
                "url": "/component/appmixer/google/spreadsheets/ListColumns?outPort=out",
                "data": {
                    // The ListColumns component needs two properties in order
                    // to get the list of columns, the Spreasheet Id and the
                    // Worksheet Id. Both will be taken from properties of the
                    // CreateRow component (the caller).
                    "properties": {
                        // Appmixer will replace 'properties/sheetId' with
                        // the actual value before making the call
                        "sheetId": "properties/sheetId",
                        "worksheetId": "properties/worksheetId"
                    },
                    // A transformer function 'columnsToInspector' from the 
                    // ListColumns.js will be executed in order to transform a list
                    // of columns to the Appmixer Inspector.
                    "transform": "./ListColumns#columnsToInspector"
                }
            }
        }
    ]
}
```

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

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

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

### Optional query parameters for the *source* call

#### ignoreAuth=true

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

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

#### silentAuth=true

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

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

## inPort.variablesPipeline

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

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

## inPort.maxConnections

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


# outPorts

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

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

![Variables Picker](/files/-L_wcaqcV6j_urLLeTrG)

An example of an `outPorts` definition can look like this:

```json
{
    "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

We support full schema definition for each option, so you can specify the structure of the data that is coming out from your component. You can add a `schema` property to each option, which contains a [JSON Schema](https://json-schema.org/) definition. For example:

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

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:

<figure><img src="/files/iufTimr8AYKxiXWb3XyW" alt=""><figcaption></figcaption></figure>

You will see the `item properties` among other variables.

<figure><img src="/files/KYm8Rc71p6Qh3lVf94dD" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/8P5rWvDZRuvQhKlruh3w" alt=""><figcaption></figcaption></figure>

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

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

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

## outPort.source

The definition is similar to the source `source` of [properties](https://docs.appmixer.com/building-connectors/manifest/pages/-LATDlWTegDtvBmHEaZx#properties.source). 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.](/files/7nAi00zh96paMezB1BIA)

Here is an example of the UpdatedRow output port definition.

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

```

## outPort.maxConnections

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


# 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](/files/-L_waiJsX3XykxpTKJiP)

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:

![Invalid Inspector field](/files/-L_wb3SCt3j399NLibhb)

![Configuration Overview](/files/-L_wb8zhnz5cwvj43-n8)

## properties.inspector

`inspector` tells the Designer UI how the input fields should be rendered. The format of this definition uses the [Rappid Inspector definition format](https://resources.jointjs.com/docs/rappid/v2.2/ui.html#ui.Inspector). Example:

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

{% hint style="warning" %}
Do not use special characters `.` or `/` in the name of the input.
{% endhint %}

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

* `type` can be any of the built-in types. See below for more details. (Custom inspector fields are also possible for on-prem installations. See the Custom Inspector Fields page for more details.)
* `group` is an identifier of an Inspector group this field belongs to. As you can see in the example above, you can have one or more custom groups (like `config` in this case) that you can define in the `groups` object. Groups will render in the Inspector UI in an accordion-like fashion. This is handy to organize your fields.
* `label` is a short text that appears above your input field. This is a great place to tell your users what your field is.

### Inspector built-in types:

#### text

A single line input field.

![](/files/-LATDq2OzXRf4RxKIFPk)

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

#### textarea

A multi-line text input field.

![](/files/-LATDq4QxkmyI1pdv0ab)

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

#### number

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

![](/files/-LATDq7rg1OIw_Fz4Sol)

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

#### key-value

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

<figure><img src="/files/eKKlvp8ZjojLw3DIMjwW" alt="" width="375"><figcaption></figcaption></figure>

```json
{
    "type": "key-value",
    "label": "An input field for key-value text pairs."
}
```

#### select

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

![](/files/-LATDqD-lMCOHwI8Y9PO)

#### multiselect

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

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

#### date-time

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

| Option          | Description                                                                                                                                                                         |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `format`        | String representing the format of the date/time. Please see the moment.js library documentation for all the available tokens: <https://momentjs.com/docs/#/parsing/string-format/>. |
| `enableTime`    | Boolean. Enables time picker.                                                                                                                                                       |
| `enableSeconds` | Boolean. Enables seconds in the time picker.                                                                                                                                        |
| `maxDate`       | String representing the maximum date that a user can pick to (inclusive).                                                                                                           |
| `minDate`       | String representing the minimum date that a user can pick to (inclusive).                                                                                                           |
| `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.                                                                                                                               |

![](/files/-LATDqGWLzxCQ2r09y7G)

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

#### toggle

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

![](/files/-LATDqJS7RS7uKJgVqkt)

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

#### color-palette

A menu of colors. Colors are defined in the `options` array each item having `content` and `value` properties, where `values` must be a color in any of the [CSS color formats](https://developer.mozilla.org/en-US/docs/Web/CSS/color) (named-color, hex-color, rgb() or hsl()).

![](/files/-LATDqQVvdKKSkH98xaS)

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

#### select-button-group

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

![](/files/-LATDqZS5y2v3QvZn6Ys)

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

![](/files/-LATDqd48FrG3HugUE_D)

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

![](/files/-LATDqiciBsQT1Es8XZd)

```
{
    "type": "select-button-group",
    "label": "Select button group with icons",
    "multi": true,
    "options": [
        { "value": "cloud", "icon": "data:image/png;base64,iVBORw0KGgoAA..." },
        { "value": "diamond", "icon": "data:image/png;base64,iVBORw0KGgoAAAA..." },
        { "value": "oval", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUh..." },
        { "value": "line", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." },
        { "value": "ellipse", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEU..." }
    ]
}
```

#### expression

A multi-field type field that allows for 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.

![](/files/-LATDqryk0yaeMkhimxK)

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

The value of this field has the following structure:

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

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

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

1. Click the `ADD` button to create a second group.
2. Select `variableA` on the `myText` field inside the first group using the variables picker.
3. When opening the variables picker in `myText` field inside the second group, only `variableB` will be available, because `variableA` is already been used.

The `expand` option in `source`. The `select` inputs in the `expression` can have dynamic values retrieved with the [`source`](#properties.source) 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:

<figure><img src="/files/rrHnJYefu0PGSGWsKm5q" alt=""><figcaption><p>source with expand</p></figcaption></figure>

This is how it is done:

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

```

#### filepicker

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

![](/files/-MOCnY79aJIhTUFDF3WU)

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

#### googlepicker

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

![](/files/-MOCoQHQiRNAAcYbcEJ8)

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

You can use *googlepicker* to pick folders instead of files:

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

This input type needs`appmixer.google.drive.GooglePicker` component to be installed.

#### onedrivepicker

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

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

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

### Conditional fields

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

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

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

* **op:** Is the operand that will be used to determine if the condition holds true or false. The following operands are supported:
  * **`eq`:** Equality between the values.
  * **`equal`:** Equality between the values by deep comparison. Used for objects and arrays.
  * **`ne`:** Values are not equal.
  * **`regex`:** Check if the value in given `field` matches the regex in the `comparisonValue`.
  * **`text`:** Check if the value in the given `field` contains the string in the `comparisonValue`.
  * **`lt`:** Check if the value in the given `field` is less than the `comparisonValue`.
  * **`lte`:** Check if the value in the given `field` is less or equal than the `comparisonValue`.
  * **`gt`:** Check if the value in the given `field` is greater than the `comparisonValue`.
  * **`gte`:** Check if the value in the given `field` is greater or equal than the `comparisonValue`.
  * **`in`:** Check if the value in the given `field` is included on the given `comparisonValue` array.
  * **`nin`:** Check if the value in the given path is not included in the given `comparisonValue`.
* **field:** The field that is used for comparison, there are several ways to reference the field:
  * **`field`:** The same form presented in the example. It will search the given fields in current input port fields.
  * **`properties/someProperty`:** Refer to a property inside component properties.
  * **`./field`:** It will refer to sibling fields of the current field. Specially useful when working with expression types.
* **comparisonValue:** The value used to compare the field against.

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

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

## properties.source

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

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

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

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

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

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

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

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

### properties.source.url

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

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

### properties.source.data.messages

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

### properties.source.data.properties

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

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

### properties.source.data.transform

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

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

Example:

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

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


# quota

Configuration of the [quota manager](/building-connectors/quotas-and-limits) 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:

```json
{
     "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](/api/user#create-user), and the account's [profileInfo](/building-connectors/authentication#requestprofileinfo-function-object-or-string-optional-2) can be used there. The following example shows how to dynamically select a `resource` based on the value of the user's [`metadata`](/api/user#create-user)`.tier` .

```json
{
     "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:

```javascript
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](/building-connectors/authentication#requestprofileinfo-function-object-or-string-optional-2).

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

## quota.resources

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

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

## quota.scope

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


# tick

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


# private

When set to `true`, the component will not be visible to 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](/building-connectors/behaviour) section, especially the [`context.getWebhookUrl()`](/building-connectors/behaviour#context-getwebhookurl) for details and example.


# httpRequestMethods

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

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


# state

Set `state` property to `{ persistent: true }` to tell the engine not to delete component state when flow is stopped. See [context.state](/building-connectors/behaviour#context-state) for more information.


# author

The author of the component. Example:

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


# marker

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

![Beta badge](/files/-M-K3NhV1AWHMnlWSaAb)

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


# localization

An optional object containing localization strings. For example:

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

For more information about component localization, refer to the [Custom Component Strings](/customizing-embedded-ui/custom-component-strings#components-manifest-localization-object) section.


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

![](/files/-LATDtrsqccOD-Js7SYR)

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

The following table lists all the possible fire pattern symbols:

| Symbol | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| \*     | (*Any)* The input port must have zero or more messages in the queue.                                                                                                                                                                                                                                                                                                                                                                                               |
| 1      | (*Exists)* The input port must have at least one message in the queue.                                                                                                                                                                                                                                                                                                                                                                                             |
| 0      | (*Empty*) The input port must have no message in the queue.                                                                                                                                                                                                                                                                                                                                                                                                        |
| A      | (*All*) The input port must have at least one message from all the connected components in the queue. This is a synchronization pattern that lets you specify that the component must wait for all the connected components to send a message before it can start processing. A typical example is a "Multiple-to-Single" join component. This component must wait for all the LoadCSV components to send a message before it can produce an SQL-like join schema. |

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

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

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


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

![](/files/-LATDmeU63VysbcQPwnd)

```javascript
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* [Unprocessed Messages](/api/unprocessed-messages) Appmixer REST AP&#x49;*.*

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

| Virtual Method       | Description                                                                                                                                                                                                                                                                                            |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **receive(context)** | Called whenever there are new messages on input ports that the component is ready to consume. This method must return a promise that when resolved, tells Appmixer that the messages were successfully processed. When rejected, the engine retries to send the messages to the component again later. |
| **tick(context)**    | Called whenever the polling timer sends a tick. This method is usually used by trigger Components to implement a API polling mechanism or for schedulers.                                                                                                                                              |
| **start(context)**   | Called when Appmixer signals the component to start (when the flow starts). This method is usually used by trigger components that might schedule an internal timer to generate outgoing messages in regular intervals or to register a webhook URL (`context.getWebhookUrl()` with a 3rd party API).  |
| **stop(context)**    | Called when Appmixer signals the component to stop (when the flow stops). This is the right place to do a graceful shutdown if necessary. Webhook-based trigger components use this place to unregister their webhook URLs with 3rd a party API.                                                       |

## Context

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

### Input/Output message(s)

#### context.messages

*(applies to* `receive()`*)*

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

![](/files/-LATDmeU63VysbcQPwnd)

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

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

![Variables](/files/-L_wgdF2TXnyQn0oDaAc)

where the flow descriptor would contain something like this:

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

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

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

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

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

#### async context.sendJson(messageContent, outPort)

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

#### async context.sendArray(arrayOfObjects, outPort)

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

```javascript
const Promise = require('bluebird');

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

### Authentication

#### context.auth

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

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

<figure><img src="/files/Ca6tRDn5I1B4pEoqI8lg" alt=""><figcaption><p>Authentication UI dialog for the above auth.js definition</p></figcaption></figure>

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:

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

### Backoffice configuration

#### context.config

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

<figure><img src="/files/5vB2f3qFHUC29n3VuLz9" alt=""><figcaption></figcaption></figure>

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

```

### Properties

#### context.properties

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

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

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

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

### Component State

#### context.state

A persistent state of the component. Sometimes you need to store data for the component that must be available across multiple `receive()` calls for the same component instance. If you also need the data to be persistent when the flow is stopped and restarted again, set the `state: { persistent: true }` property in your component manifest, otherwise, the `context.state` will be cleared when the flow containing the component stops.

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

```javascript
{
    async receive(context) {
        // Emit a message only once per day for this component instance.
        const day = (new Date).getDay();
        const state = context.state;
        if (!state[day]) {
            state[day] = true;
            await context.saveState(state);
            return context.sendJson({ tick: true }, 'out');
        }
    }
}
```

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

{% hint style="info" %}
The `context.state` object should not be used to store large amounts of data. The state is loaded with each received message on a component input port. The maximum limit is 16MB but storing such large objects will heavily slow down the processing of the component input messages.
{% endhint %}

#### async context.loadState()

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

#### async context.saveState(object)

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

#### async context.stateSet(key, value)

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

#### async context.stateGet(key)

Get a state value stored under `key`.

#### async context.stateUnset(key)

Remove a value under `key`.

#### async context.stateClear()

Clears the entire state.

#### async context.stateAddToSet(key, value)

Add value into set under `key`.

#### async context.stateRemoveFromSet(key, value)

Remove value from set under `key`.

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

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

### Flow State

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

#### async context.flow\.loadState()

Load the state from the DB.

#### async context.flow\.stateSet(key, value)

Set a state `key` to hold the `value`. `key` must be a string. `value` can be a string, number or a JSON object.

#### async context.flow\.stateGet(key)

Get a state value stored under `key`.

#### async context.flow\.stateUnset(key)

Remove a value under `key`.

#### async context.flow\.stateClear()

Clears the entire state.

#### async context.flow\.stateAddToSet(key, value)

Add `value` into a *Set* stored under `key`.

#### async context.flow\.stateRemoveFromSet(key, value)

Remove `value` from *Set* stored under `key`.

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

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

### Service State

This is similar to the component state, but the service state is available across all components in the connector.

#### async context.service.loadState()

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

#### async context.service.stateSet(key, value)

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

#### async context.service.stateGet(key)

Get a state value stored under `key`.

#### async context.service.stateUnset(key)

Remove a value under `key`.

#### async context.service.stateClear()

Clears the entire state.

#### async context.service.stateAddToSet(key, value)

Add `value` into a *Set* stored under `key`.

```javascript
module.exports = {

    async start(context) {

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

    async stop(context) {

        return context.service.stateRemoveFromSet(
            context.properties.channelId,
            {
                componentId: context.componentId,
                flowId: context.flowId
            }
        );
    }
}
```

#### async context.service.stateRemoveFromSet(key, value)

Remove `value` from *Set* stored under `key`.

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

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

### Files

#### async context.saveFile(fileName, mimeType, buffer)

**This method has been deprecated. Use** [**context.saveFileStream** ](#async-context.savefilestream-filename-stream)**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.

```javascript
{
    receive(context) {
        // getAttachment() is an example function that retrieves a file from an API
        return getAttachment(context.auth, context.messages.attachment.content.id)
            .then((file) => {
                return context.saveFile(file.name, file.mimeType, Buffer.from(file.data, 'base64'));
            })
            .then((result) => {
                return context.sendJson({ fileId: result.fileId }, 'file');
            });
    }
}
```

#### async context.saveFileStream(fileName, stream)

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

The structure of the returned object looks like this:

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

See, for example, the [AWS S3 GetFileObject](https://github.com/clientIO/appmixer-connectors/blob/19f93427390fc4bf58a7137784e8626457b7ea0e/src/appmixer/aws/s3/GetFileObject/GetFileObject.js#L17) component for an example of how `safeFileStream()` can be used.

#### async context.replaceFileStream(fileId, stream)

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

Return object:

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

```javascript
'use strict';

module.exports = {

    receive(context) {

        const { fileId, content } = context.messages.in.content;
        return context.replaceFileStream(
            fileId,
            content
        ).then(savedFile => {
            return context.sendJson(savedFile, 'file');
        });
    }
};

```

#### async context.getFileInfo(fileId)

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

Example return object:

```javascript
{
  "filename": "testFile",
  "contentType": "text",
  "length": 7,
  "chunkSize": 261120,
  "uploadDate": "2021-01-22T12:20:29.227Z",
  "metadata": {
    "userId": "5f804b96ea48ec47a8c444a7",
    "fileId": "fd0e9149-3249-4d42-b519-bfd9ab6773c5"
  },
  "md5": "9a0364b9e99bb480dd25e1f0284c8555",
  "fileId": "fd0e9149-3249-4d42-b519-bfd9ab6773c5"
}
```

#### async context.loadFile(fileId)

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

```javascript
{
    receive(context) {
        return context.loadFile(context.messages.file.content.fileId)
            .then((fileContent) => {
                // uploadFileToAPI() is some function that uploads a file to an API
                return uploadFileToAPI(context.auth, content.messages.file.content.fileName, fileContent);
            });
    }
}
```

#### context.readFileStream(fileId)

**This method has been deprecated. Use** [**context.getFileReadStream**](#async-context.getfilereadstream-fileid) **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)`.

#### async context.getFileReadStream(fileId)

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

#### async context.removeFile(fileId)

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

### Webhook

#### context.getWebhookUrl()

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

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

```

{% hint style="info" %}
Note: The `context.getWebhookUrl()` is only available if you set `webhook: true` in your component manifest file (component.json). This tells Appmixer that this is a "webhook"-type of component.
{% endhint %}

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

| Property           | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `content.method`   | HTTP method of the request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `content.hostname` | Hostname of the Appmixer API.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `content.headers`  | HTTP headers of the request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `content.query`    | Object with query parameters, i.e. query string parsed into a JSON object.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `content.data`     | Object with the body parameters of the request.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `correlationId`    | A special ID generated by Appmixer that uniquely identifies the input message which resulted in generating the webhook URL. In other words, if you call `context.getWebhookUrl()` in the `receive()` method in a reaction to an input message that arrived on an input port of the webhook component, the `correlationId` will be part of the returned webhook URL. This allows you to later associate the input message with the HTTP call to the webhook. A common pattern is to store the input message in the `context.state` object and later use the `context.messages.webhook.correlationId` to retrieve it back. For example, if you have an input port named `myInPort`, you can get the `correlationId` of the input message that just arrived by accessing the `context.messages.myInPort.correlationId.` |

#### async context.response(body, statusCode, headers)

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

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

### HTTP

#### **async context.httpRequest**

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 [axios](https://axios-http.com/docs/api_intro) library.

```javascript
module.exports = {
  async receive(context) {
    const { data } = await context.httpRequest({
      url: "https://some-url.com/api",
      method: "POST",
      data: {
        username: "someuser",
        password: "somepass",
      },
      headers: { Authentication: "some"}        
    });
        
    // it is also possible to execute dedicated http methods
    // see here https://axios-http.com/docs/api_intro
    // for example
    // context.httpRequest.get(url[, config])
    const { data } = await context.httpRequest.get("https://url", { params: { name: "some" }, headers: {}});
    // context.httpRequest.post(url[, data[, config]])
    const { data } = await context.httpRequest.post("https://url", { username: "some" }, headers: {});
        
    return context.response(data);
  }
}
```

### Store

#### async context.store.listStores()

Get the list of user's Data Stores.

#### async context.store.get(storeId, key)

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

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

Set value to the Data Store under the *key.*

#### async context.store.remove(storeId, key)

Remove the *key* from the Data Store.

#### async context.store.clear(storeId)

Clear all data from the Data Store.

#### async context.store.find(storeId, query)

Find items in the Data Store.

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

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

Get a cursor.

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

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

```javascript
module.exports = {

    async receive(context) {

        // whenever there is an item added/updated/removed from the data
        // store, this component will get triggered with data on the
        // context.messages.webhook.content
        const data = context.messages.webhook.content.data.currentValue;

        if (context.messages.webhook.content.data.type === 'insert') {
            await context.sendJson({
                key: data.key,
                storeId: data.storeId,
                value: data.value,
                updatedAt: data.updatedAt,
                createdAt: data.createdAt
            }, 'item');
        }
        return context.response('ok');
    },

    async start(context) {

        // register without specifying 'events'.
        await context.store.registerWebhook(context.properties.storeId);
    },

    async stop(context) {

        await context.store.unregisterWebhook(context.properties.storeId);
    }
};
```

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

```javascript
module.exports = {

    async receive(context) {

        const data = context.messages.webhook.content.data.currentValue;
        await context.sendJson({
            key: data.key,
            storeId: data.storeId,
            value: data.value,
            updatedAt: data.updatedAt,
            createdAt: data.createdAt
        }, 'item');
        return context.response('ok');
    },

    async start(context) {

        // register only the 'insert' events on the data store.
        await context.store.registerWebhook(context.properties.storeId, ['insert']);
    },

    async stop(context) {

        await context.store.unregisterWebhook(context.properties.storeId);
    }
};
```

#### async context.store.unregisterWebhook(storeId);

Unregister a webhook.

### Scheduling

#### async context.setTimeout(messageContent, delay)

Set a timer that causes the component to receive `messageContent` in the `receive()` method in the special `context.messages.timeout.content` object. `delay` is the time, in milliseconds, the timer should wait before sending the `messageContent` to the component itself. This is especially useful for any kind of scheduling components.

The `context.setTimeout()` function works in a cluster environment as opposed to using the global `setTimeout()` JavaScript function. For example, a component that just *slows down* incoming messages before sending them to its output port, waiting e.g. 5 minutes, can look like this:

```javascript
module.exports = {
    receive(context) {
        if (context.messages.timeout) {
            // Timeout message.
            return context.sendJson(context.messages.timeout.content, 'out');
        } else {
            // Normal input message.
            return context.setTimeout(context.messages.in.content, 5 * 60 * 1000);
        }
    }
};
```

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

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

#### async context.clearTimeout(timeoutId)

Clear (cancel) a scheduled timeout.

### Miscellaneous

#### async context.callAppmixer(request)

Call an Appmixer REST API endpoint. You can call any of the Appmixer endpoints defined in the [API section](broken://pages/-Lvp85ds_2Oc4usHZMUn). 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:

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

#### async context.stopFlow()

Stop the running flow. Example:

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

#### context.componentId

The ID of the component.

#### context.flowId

The ID of the flow the component runs in.

#### context.flowDescriptor

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

#### context.customFields

Flow [*customFields*](/tutorials/flows-metadata-and-filtering#setting-custom-metadata) properties are available in this object.

#### context.evalJavaScript(code, jsonData)

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

```javascript
'use strict';

module.exports = {

    receive(context) {

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

        let sum = 0;

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

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

#### context.setMaxWait(timestamp)

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

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

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

#### async context.loadVariables()

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

```json
[
  {
    "variables": {
      "dynamic": [
        {
          "label": "Column A",
          "value": "columnA",
          "componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
          "port": "out"
        },
        {
          "label": "Column B",
          "value": "columnB",
          "componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
          "port": "out"
        }
      ],
      "static": {}
    },
    "sourceComponentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
    "outPort": "out",
    "inPort": "in"
  },
  {
    "variables": {
      "dynamic": [
        {
          "label": "Column C",
          "value": "columnC",
          "componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
          "port": "out"
        },
        {
          "label": "Column D",
          "value": "columnD",
          "componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
          "port": "out"
        }
      ],
      "static": {}
    },
    "sourceComponentId": "71eb1f40-ce29-4963-8922-102739bafee4",
    "outPort": "out",
    "inPort": "in"
  }
]
```

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

Example:

```javascript
{
    receive(context) {
        return context.loadVariables()
            .then(data => {
                const newSchema = data.reduce((acc, item) => {
                    return acc.concat(item.variables.dynamic.map(o => ({ label: o.label, value: o. value })));
                }, []);
                context.sendJson(newSchema, 'leftJoin');
                context.sendJson(newSchema, 'innerJoin');
                context.sendJson(newSchema, 'rightJoin');
            });
    }
}
```

#### async context.log(object)

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

Example:

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

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

![](/files/-METzGz3Tjo53CRzwgQS)

#### async context.lock(lockName, options)

This method allows components to create a cluster lock. This is useful when creating a mutually exclusive section inside the component's code. Such a thing can be achieved in Appmixer using either [quota](/building-connectors/quotas-and-limits) (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:

```javascript
    async receive(context) {

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

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

            await context.saveState({ callCount, messages });
        } finally {
            if (lock) {
                await lock.unlock();
            }
        }
    }

```

### Error Handling

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

#### receive(context)

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 [`unprocessedMessages`](/api/unprocessed-messages) 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.

```javascript
/**
 * Example of context.CancelError
 */
module.exports = {

    async receive(context) {

        let data = context.messages.in.content;

        try {
            // In this component is trying to create a record in a 3rd party
            // system.
            const resp = await someAPI.createSomething(data);
            await context.sendJson(resp, 'out');
        } catch (err) {
            // And there might be a unique constraint, let's say an email
            // address. And the 3rd party API will return an error with a
            // message saying that this record cannot be created.
            if (err.message === 'duplicate record') {
                // In this case, we can tell Appmixer to cancel the message.
                // Because next attempt would fail again with the same result.
                throw new context.CancelError(err.message);
            }
            // In case of any other error, rethrow the exception. Appmixer will
            // then try to process it again.
            throw err;
        }
    }
};

```

#### tick(context)

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

#### start(context)

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

#### stop(context)

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


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

{% hint style="info" %}
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.
{% endhint %}

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


# 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](https://en.wikipedia.org/wiki/Application_programming_interface_key), [Password](#password), [OAuth 1](https://oauth.net/1/), and [OAuth 2](https://oauth.net/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).

### type

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

### definition

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

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

## Authentication mechanisms

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

### function, object, or a URL string

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

```javascript
// 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 ...).
    }
}

```

### API key

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

```javascript
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;
        }
    }
};
```

Next, we explain the fields inside the definition object:

#### auth (object)

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

![Authentication form for Freshdesk, defined by auth object](/files/-MGoWzN5x3CaNNTs6cfE)

The values configured by the user will be exposed in the [`context`](/building-connectors/behaviour#context) object with the same keys as in the *auth* object. In this case, we will be able to access the values as `context.domain` and `context.apiKey`in our component [NodeJS module](/building-connectors/behaviour).

#### requestProfileInfo ([function, object or string](#function-object-or-a-url-string)) (optional)

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

See [examples of real connectors](https://github.com/search?q=repo%3AclientIO%2Fappmixer-connectors+requestProfileInfo\&type=code) that implement this method.

#### accountNameFromProfileInfo ([function or string](#function-object-or-a-url-string))

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:

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

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

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

#### validate ([function, object or string](#function-object-or-a-url-string))

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:

```javascript
'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}}'
            }
        }
    }
};

```

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

![](/files/TUyqeWrABk29hOGdws26)

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:

```javascript
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;
}
```

## Password

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

```javascript
'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 };
        }
    }
};

```

#### HTTP Basic Authentication

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

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

Then the *username* and *password* is available in the components:

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

### OAuth 1

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

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

module.exports = {

    type: 'oauth',

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

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

        return {

            accountNameFromProfileInfo: 'id',

            authUrl: context => {

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

            requestRequestToken: () => {

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

            requestAccessToken: context => {

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

            requestProfileInfo: context => {

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

            validateAccessToken: context => {

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

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

```

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

#### accountNameFromProfileInfo ([function or string](#function-object-or-a-url-string))

Works exactly the same way as described in the [API Key](#accountnamefromprofileinfo) section.

#### requestRequestToken ([function, object or string](#function-object-or-a-url-string))

This must be a function (or an object, or just a string URL as explained [here](#authentication-mechanisms)) that returns a promise which must resolve to an object containing `requestToken` and `requestTokenSecret` the same way that is shown in the example above. These are needed to get the access token and become exposed by the context - `context.requestToken` and `context.requestTokenSecret`.

#### requestAccessToken ([function, object or string](#function-object-or-a-url-string))

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

#### authUrl ([function, object or string](#function-object-or-a-url-string))

[Function, object or a string](#authentication-mechanisms) URL returning *auth* URL. Appmixer will then use this URL to redirect the user to the proper authentication page. The `requestToken` is available in the *context*. The example shows the *authUrl* declaration using the token provided by the *context*.

#### requestProfileInfo ([function, object or string](#function-object-or-a-url-string)) (optional)

Works exactly the same way as described in the [API Key](#requestprofileinfo-optional) section.

#### validateAccessToken ([function or object](#function-object-or-a-url-string))

This property serves the same purpose as [validate](#validate) 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.

### OAuth 2

The latest OAuth protocol and industry-standard, OAuth 2.0 improved many things from the first version in terms of usability and implementation, while maintaining a high degree of security. It is easier to implement in Appmixer as well. In order to use this mechanism, `type` property must be set to `oauth2`. Here is an example from [Asana `auth.js` module](https://github.com/clientIO/appmixer-connectors/blob/master/src/appmixer/asana/auth.js):

```javascript
'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}}'
                }
            }
        };
    }
};

```

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:

#### accountNameFromProfileInfo ([function or string](#function-object-or-a-url-string))

Works exactly the same way as described in the [API Key](#accountnamefromprofileinfo) section.

#### authUrl ([function, object or string](#function-object-or-a-url-string))

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:

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

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 [JIRA `auth.js` module definition](https://github.com/clientIO/appmixer-connectors/blob/master/src/appmixer/jira/auth.js#L26).

The same logic applies to the following property `requestAccessToken`.

#### requestAccessToken ([function, object or string](#function-object-or-a-url-string))

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, [the JIRA `auth.js` module definition](https://github.com/clientIO/appmixer-connectors/blob/master/src/appmixer/jira/auth.js#L56).

#### requestProfileInfo ([function, object or string](#function-object-or-a-url-string)) (optional)

Works exactly the same way as described in the [API Key](#requestprofileinfo-optional) section.

#### refreshAccessToken ([function, object or string](#function-object-or-a-url-string))

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

#### validateAccessToken ([function, object or string](#function-object-or-a-url-string))

Has the exact same purpose as the same method in the [OAuth 1](#validateaccesstoken).

#### scope

String or an array of strings.

#### scopeDelimiter

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

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

```javascript
'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}}'
            }
        }
    }
};

```

#### refreshBeforeExp

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

```javascript
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'
        
        ...
    }
}
```

#### refreshBeforeExpUnits

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

#### OAuth 2 redirect URI

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

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

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

<figure><img src="/files/tp2wmxy58rU58nXeD7UY" alt=""><figcaption><p>Setting custom redirect URI just for appmixer:google module.</p></figcaption></figure>

### Context

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

#### async context.httpRequest

Wrapper around the [axios](https://axios-http.com/docs/api_intro) library, making it easy to initiate HTTP requests without the need to import a 3rd party library:

```javascript
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;
        }
    }
};
```

#### async context.log(string severity, object)

Just like in a component, this can be used to create a log record that is visible in the Insights. This is useful for debugging auth.js files.

### Custom "Connect account" button

Appmixer allows you to redefine the *Connect Account* button in the Designer and Integration Wizards. This is especially useful if the 3rd party has specific requirements for branding of the sign-in buttons, such as [Google](https://developers.google.com/identity/branding-guidelines).

<figure><img src="/files/ZNJSU5jFb5cY3FZO2D4G" alt=""><figcaption><p>Custom "Connect account" button in the Designer.</p></figcaption></figure>

<figure><img src="/files/qDO3V02EK8A9HWm1Au6D" alt=""><figcaption><p>Custom "Connect account" button in the Wizard.</p></figcaption></figure>

This is done with an optional `connecAccountButton: { image: 'data uri' }` property. Example from the [Google auth.js](https://github.com/clientIO/appmixer-connectors/blob/master/src/appmixer/google/auth.js):

```javascript
'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;
            },
            
    ...
}

```

### Setting OAuth 1,2 secrets

To set your OAuth applications secrets, follow the [Connector Configuration](/getting-started/services#custom-oauth-credentials) guideline.


# 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.js`or `[vendor/[service]/[module]/[component]/quota.js`.

An example of a quota module:

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

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

## Quota Module Structure

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

### rules

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

#### limit

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

#### window

The time window in milliseconds.

#### throttling

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

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

#### resource

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

{% hint style="info" %}
You can also configure system webhook to receive the quota errors when raised. Read more about it [here](https://docs.appmixer.com/appmixer/appmixer-self-managed/system-webhooks).
{% endhint %}


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

### Understanding MCP Server Integration in Appmixer

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

<figure><img src="/files/9G09hYj62SGzzNSRWxyZ" alt=""><figcaption></figcaption></figure>

***

### MCP Server Connector Components

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

| **Component** | **Purpose**                                                                         | **Where it’s used**                                                                                                                                  |
| ------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **MCPServer** | Core component that provides the actual MCP server connection and tool definitions. | Can be connected only to AI Agent component’s mcp port.                                                                                              |
| **ListTools** | Helper component that lists all available tools exposed by the MCP server.          | Used internally by MCPServer (not standalone).                                                                                                       |
| **CallTool**  | Helper component to invoke a specific tool from the MCP server.                     | Used internally by MCPServer and can also be used standalone in regular flows to call a selected tool, similar to an action-type Appmixer component. |

Important:

* The **AI Agent component** `mcp` port can connect **only** to *MCPServer* components.
* *ListTools* and *CallTool* are *helper* components for the *MCPServer*, but *CallTool* has the added ability to be used independently in non-AI Agent flows.

***

### How to Implement a Custom MCP Server Connector<br>

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.

#### Step 1 – Start from an Existing Example

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

* Repository: [appmixer-connectors/src/appmixer/mcpservers](https://github.com/clientIO/appmixer-connectors/tree/dev/src/appmixer/mcpservers)
* Example: [GitLab MCP Server Connector](https://github.com/clientIO/appmixer-connectors/tree/dev/src/appmixer/mcpservers/modelcontextprotocol_server_gitlab)

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

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

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

#### Step 2 – Files to Modify

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

1. **lib.js**
   * Points to the NPM package containing your MCP server. Your MCP server must be NodeJS based.\
     \&#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>`
     * 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.

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

***

### Packaging and Publishing

Once you have updated the connector, follow the standard instructions to pack and publish your connector to your Appmixer tenant as described [here](/getting-started/custom-connectors#publish-your-component).

```
appmixer pack appmixer/mcpservers/your_mcp_server
appmixer publish appmixer.mcpservers.your_mcp_server.zip
```

***

### Summary Checklist

\
Before publishing:

* Updated `package.json` with correct name and NPM MCP server dependency.
* Updated `module.json`, `bundle.json` with correct name, metadata and icons.
* Edited `lib.js` to point to your MCP server code.
* Configured `auth.js` with required authentication fields.
* Updated `component.json` files for all three components.
* Tested `CallTool` both as helper and standalone in a flow.
* Connected `MCPServer` to an AI Agent `mcp` port and verified tool discovery.


# Example: twilio.SendSMS

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

![](/files/-Lzc8O5OEoovG9V-zqyH)

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

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

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

```
{
    "name": "appmixer.twilio",
    "version": "1.0.0",
    "dependencies": {
        "twilio": "^3.14.0"
    }
}
```

## Helper Component (sms/ListFromNumbers/ListFromNumbers.js)

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

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

![Variables dynamically populated at design time with available Twilio phone numbers.](/files/-M17UmQLVPdH5zxKW5nl)

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

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

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

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

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

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

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

```javascript
module.exports.fromNumbersToSelectArray = (numbers) => {
    let transformed = [];
    if (Array.isArray(numbers)) {
        numbers.forEach(number => {
            transformed.push({
                label: number['phoneNumber'],
                value: number['phoneNumber']
            });
        });
    }
    return transformed;
};
```


# 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:<br>

* Authentication: API key based authentication. All requests must provide the `X-Api-Key` HTTP header that contains an API key that identifies a particular user.
* `POST /webhooks` HTTP endpoint with `{ url: SOME_URL }` payload to register a new webhook URL that will be called by the Todo API server every time there's a change in the user's todo list (new todo item, deleted todo item, updated todo item). This endpoint returns the webhook ID in its response (`{ id: WEBHOOK_ID }`).
* `DELETE /webhooks/:id` HTTP endpoint that unregisters a previously registered webhook identified by its ID.
* `GET /me` HTTP endpoint that returns the user's profile.
* Webhook callbacks: Our TODO API server calls registered webhooks every time there's a change in the user's todo list. The following events (webhook request payloads) are supported:
  * `{ event: "todo-created", todo: { label: String, id: String } }`
  * `{ event: "todo-deleted", todo: { label: String, id: String } }`
  * `{ event: "todo-updated", todo: { label: String, id: String } }`

### Appmixer Service Definition

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

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

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

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

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

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

### Service Manifest (service.json)

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

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

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 <https://docs.appmixer.com/appmixer/appmixer-self-managed/installation#enabling-users-to-publish-custom-components> 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:<br>

![](/files/MmtfEzEljzE1mmpfxQoq)

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 <https://en.wikipedia.org/wiki/Data_URI_scheme>) and the connector is displayed in the collapsible "Applications" category in the left panel.

### Service Authentication Module

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

![](/files/PwS8YLdBs52mfPs2ljdN)

Our `auth.js` file looks like this:

```javascript
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}}'
            }
        }
    }
};
```

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 <https://docs.appmixer.com/appmixer/component-definition/authentication#authentication-module-structure> 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:

![](/files/vVcdWssDNgVt01UpJYeY)

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: <https://github.com/axios/axios#request-config>). As you can see, you can use fields from the `definition.auth` object by enclosing them with the `{{` and `}}` brackets. We take advantage of that by injecting the user provided api key in the `X-Api-Key` HTTP header. Moreover, notice the `url` field and the use of `{{config.baseUrl}}`. The `{{config.CONFIG_KEY}}` allow us to use service configuration values defined in the Backoffice admin panel. This is very handy if you don't want to hardcode certain values in your service/module/component definition and instead, make those values configurable via Backoffice. To do that, visit your Appmixer tenant Backoffice interface, go to "Services", click "Add" to add a new service configuration, provide the correct service ID in the form `[vendor]:[service]` and add your custom configuration key/values by clicking on the "burger" icon:

<br>

![](/files/PP9MBjsm5wrmfBiGOXNR)

![](/files/B1x7VZMOrnPwELAgFRd7)

![](/files/W1rJmEaIkkoYMT8WPhUO)

![](/files/BGcRqSspUrYJ6JQ6TEro)

### Component manifest (component.json)

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

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

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

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

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

![](/files/2GirmOYafGZvf31SFoMG)

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:

![](/files/wiW0t48uuwgTqpvs1h3I)

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

### Component Behaviour (NewTodo.js)

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

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

The module exports functions known to the Appmixer engine that the Appmixer engine calls at the right times. See Component Behaviour for more details: <https://docs.appmixer.com/appmixer/component-definition/behaviour>. 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).
* `stop(context)` that the Appmixer engine calls when the flow stops. This is the place where we unsubscribe our webhook from the Todo API to properly clean up after ourselves (remove the webhook from the Todo API).

### Packing and Publishing our Service

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 (<https://docs.appmixer.com/appmixer/appmixer-cli/appmixer-cli>). In short, the process is pretty straightforward and looks like this:

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

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

![](/files/wfLFUD7CXo8B4bE2WiqG)

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:

![](/files/2ZxNjU3dheMRhie8Jc7q)

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

### Download the demo Service

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

{% file src="/files/U9VZ8fZ7CZE4jM0Ydnsm" %}


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

<figure><img src="/files/8taYLX6wj1uBkzMNzu58" alt=""><figcaption></figcaption></figure>


# Installation

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

### Basic Usage

Load `appmixer.js` UMD module in your HTML file:

```html
<script src="https://my.YOUR_TENANT.appmixer.cloud/appmixer/appmixer.js"></script>

<script type="module">
const appmixer = new Appmixer({ baseUrl: 'https://api.YOUR_TENANT.appmixer.cloud' })
appmixer.api.authenticateUser(username, password).then(auth => {
    appmixer.set('accessToken', auth.token);
    ...
    const integrations = new appmixer.ui.Integrations({ el: '#integrations' });
    integrations.open();
});
</script>
```

See the following repositories for more comprehensive demos on how Appmixer can be embedded in your apps <https://github.com/clientIO/appmixer-demo-embedded-integrations>, <https://github.com/clientIO/appmixer-demo-firebase-vanilla>.

{% hint style="info" %}
If you're using a Self-Managed Appmixer package, you should link your own Appmixer JavaScript SDK from your own Appmixer Studio URL (i.e. instead of <https://my.YOUR\\_TENANT.appmixer.cloud>, you will reference your own Studio URL). Alternatively, you can download the appmixer.js file and link to it from whatever location you will put it in.
{% endhint %}

### Advanced Usage

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

```shell
wget https://my.YOUR_TENANT.appmixer.cloud/appmixer/package/appmixer.es.js
wget https://my.YOUR_TENANT.appmixer.cloud/appmixer/package/appmixer.css
```

```javascript
import { Appmixer } from './appmixer.es.js'
import './appmixer.css'

const appmixer = new Appmixer(/* ... */)
```

Choose Appmixer UI widgets to include:

```javascript
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(/* ... */)
```


# Quick Start

Refer to the [Embed into Your Application](/getting-started/embed) getting started guide to get started quickly.


# Constructor

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

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const appmixer = new Appmixer({/* [name]: value */})

appmixer.set(name, value)
appmixer.get(name)
```

#### **`baseUrl`**

Type: `String` | Default: `null`

Base URL of your Appmixer engine REST API.

#### **`accessToken`**

Type: `String` | Default: `null`

Access token of an authorized user.

#### **`debug`**

Type: `Boolean` | Default: `false`

Enable debugger for development purposes.

#### **`theme`**

Type: `Object` | Default: `DefaultTheme`

#### **`l10n`**

Type: `Object` | Default: `DefaultL10N`

Define custom localization texts.

#### **`lang`**

Type: `String` | Default: `en`

Specify a language code for the localization of components.

#### **`api`**

Type: `Object` | Default: `{}`

Set custom API methods.

## Instance <a href="#instance" id="instance"></a>

#### **`appmixer.ui`**

```javascript
appmixer.ui('Widget', {/* ... */})
appmixer.ui.Widget({/* ... */})
```

Register and create UI Widgets.

#### **`appmixer.api`**

Use methods of built-in API Module.

#### **`appmixer.set`**

```javascript
appmixer.set(key, value)
```

Set configuration property.

#### **`appmixer.get`**

```javascript
appmixer.get(key, value)
```

Get configuration property.

#### **`appmixer.registerCustomComponentShape`**

```javascript
appmixer.registerCustomComponentShape(name, shape)
```

Register a custom **Designer** component shape.

#### **`appmixer.registerInspectorField`**

```javascript
appmixer.registerInspectorField(type, Field, options)
```

Register a custom **Designer** inspector field.


# 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. |
| `api.on(event, handler)`  | Add event listener.         |
| `api.off(event, handler)` | Remove event listener.      |

## Methods

#### `api.authenticateUser`

```javascript
await appmixer.api.authenticateUser(username, password)
```

Authenticate a user to Appmixer. Note that this can be a "virtual" user that exists for the sole purpose of associating a real user of your own product to a user in Appmixer. Each user in Appmixer can have a set of flows, can run and stop flows, and can see data going through their flows. The returned promise is either resolved with an object that contains a token (which you need to set with `appmixer.set('accessToken', token)` to be able to make calls to the API backend. Or the promise is rejected with an error object. If the error object returns a 403 status code (i.e. `err.response.status === 403`), the user does not exist in Appmixer.

#### `api.authenticateWithEmailAndPassword`

```javascript
await appmixer.api.authenticateWithEmailAndPassword(email, password)
```

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

#### `api.signupUser`

```javascript
await appmixer.api.signupUser(username, password, [email])
```

Create a new user in Appmixer. The returned promise is either resolved with an authentication object (containing the `token` property) or rejected if the sign-up fails. If the `email` parameter is not provided, then the `username` will be copied as email.

#### `api.createFlow`

```javascript
await appmixer.api.createFlow(name, [descriptor], [properties])
```

`appmixer.api.createFlow(name, [descriptor], [properties])`Create a new flow in Appmixer. The returned promise resolves to the ID of the newly created flow. The `properties` object can contain your own custom metadata inside the `customFields` property. This is especially useful for filtering flows based on your own custom metadata.

#### `api.deleteFlow`

```javascript
await appmixer.api.deleteFlow(flowId)
```

Delete an existing flow identified by `flowId`.

#### `api.getFlow`

```javascript
await appmixer.api.getFlow(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 [Data URI](https://en.wikipedia.org/wiki/Data_URI_scheme) format).

#### `api.getFlows`

```javascript
await appmixer.api.getFlows(query)
```

Get all flows of the user or filter them by `query`. `query` is an object with the following properties: `limit`, `offset`, `pattern` (a string to filter flows containing pattern in their names), `sort`, `projection` (allows you to exclude properties from the returned flow objects), `sharedWithPermissions` and `filter`.Example:

```json
{
  limit: 20,
  offset: 0,
  pattern: "slack",
  projection: "-flow,-thumbnail",
  sort: "mtime:-1",
  sharedWithPermission: "read",
  filter: "userId:423jfdsalfjl4234fdsa"
}
```

#### `api.getFlowsCount`

```javascript
await appmixer.api.getFlowsCount(query)
```

Get the number of all flows of the user or filter them by `query`. `query` is an object with `pattern` property that can include a string to filter flows containing a pattern in their names. Example: `{ "pattern": "dropbox" }`.

#### `api.updateFlow`

```javascript
await appmixer.api.updateFlow(flowId, update)
```

Update an existing flow. `update` can contain the following information: `{ flow, name, customFields }`, where `flow` is the Flow Descriptor of the flow and `customFields` is an object with your own custom metadata for this flow.

#### `api.startFlow`

```javascript
await appmixer.api.startFlow(flowId)
```

Start a flow.

#### `api.stopFlow`

```javascript
await appmixer.api.stopFlow(flowId)
```

Stop a flow.

#### `api.cloneFlow`

```javascript
await appmixer.api.cloneFlow(flowId)
```

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

#### `api.getUser`

```javascript
await appmixer.api.getUser()
```

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

#### `api.getStores`

```javascript
await appmixer.api.getStores()
```

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

#### `api.getStore`

```javascript
await appmixer.api.getStore(storeId)
```

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

#### `api.getStoreRecordsCount`

```javascript
await appmixer.api.getStoreRecordsCount(query)
```

Get the number of records in a store. `query` is an object with `storeId` and `pattern` properties where `pattern` is a string to filter records that contain the string in their keys or values.

#### `api.getStoreRecords`

```javascript
await appmixer.api.getStoreRecords(query)
```

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

```json
{
  limit: 30,
  offset: 0,
  pattern: "foo",
  sort: "updatedAt:-1",
  storeId: “5c6d643f4849f447eba55c1d"
}
```

#### `api.createStore`

```javascript
await appmixer.api.createStore(name)
```

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

#### `api.deleteStore`

```javascript
await appmixer.api.deleteStore(storeId)
```

Delete a store.

#### `api.renameStore`

```javascript
await appmixer.api.renameStore(storeId, newName)
```

Rename an existing store.

#### `api.createStoreItem`

```javascript
await appmixer.api.createStoreItem(storeId, key, value)
```

Create a new record in a store.

#### `api.deleteStoreItems`

```javascript
await appmixer.api.deleteStoreItems(items)
```

Delete store items. `items` is an array of objects each having a `key` and `storeId` properties identifying the item and store from which the item should be removed.

#### `api.createAccount`

```javascript
await appmixer.api.createAccount(params, data)
```

Create a custom account.

#### `api.getAccounts`

```javascript
await appmixer.api.getAccounts(filter)
```

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

#### `api.getComponentAccounts`

```javascript
await appmixer.api.getComponentAccounts(componentType, componentId)
```

Get a list of accounts connected to a specific component.

#### `api.getAccountFlows`

```javascript
await appmixer.api.getAccountFlows(accountId)
```

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

#### `api.setAccountName`

```javascript
await appmixer.api.setAccountName(accountId, newName)
```

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

#### `api.getLogs`

```javascript
await appmixer.api.getLogs(query)
```

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

```json
{
  from: 0,
  size: 30,
  sort: "@timestamp:desc",
  query: "@timestamp:[2018-01-01 TO 2018-01-01]"
}
```

Get logs of a specific flow:

```json
{
  from: 0,
  size: 30,
  sort: "@timestamp:desc",
  query: "@timestamp:[2018-01-01 TO 2018-01-01] AND +flowId:FLOW_ID"
}
```

#### `api.getLog`

```javascript
await appmixer.api.getLog(logId, index)
```

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

#### `api.getPeopleTasks`

```javascript
await appmixer.api.getPeopleTasks(query)
```

Get all tasks of the user. `query.role` can be either "approver" or "requester" and allows you to filter tasks based on the role. `query.pattern` filters returned tasks by a term that must be contained in the task title. Setting`query.secret` to either the `approverSecret` or `requesterSecret` allows you to get a list of tasks of a different user for which you have the secret (other than the one identified by the access token, i.e. the currently signed-in user).

#### `api.getPeopleTasksCount`

```javascript
await appmixer.api.getPeopleTasksCount(query)
```

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

#### `api.getPeopleTask`

```javascript
await appmixer.api.getPeopleTask(id)
```

Return one task identified by `id`.

#### `api.approveTask`

```javascript
await appmixer.api.approveTask(id, [params])
```

Approve a task identified by `id`. `params` is an optional object that can contain the `secret` property (approver secret). Having the secret allows you to approve a task of any user for which you have the secret, not just the currently signed-in user.

#### `api.rejectTask`

```javascript
await appmixer.api.rejectTask(id, [params])
```

Reject a task identified by `id`. `params` is an optional object that can contain the `secret` property (approver secret). Having the secret allows you to reject a task of any user for which you have the secret, not just the currently signed-in user.

#### `api.getCharts`

```javascript
await appmixer.api.getCharts()
```

Returns all the Insights charts of the user.

#### `api.getChart`

```javascript
await appmixer.api.getChart(chartId)
```

Return one Insights chart identified by `chartId`.

#### `api.deleteChart`

```javascript
await appmixer.api.deleteChart(chartId)
```

Delete an Insights chart identified by `chartId`.

#### `api.getFlowAuthentication`

```javascript
await appmixer.api.getFlowAuthentication(flowId)
```

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

## Events <a href="#events" id="events"></a>

```javascript
appmixer.api.on(event, handler)
```

#### **`error`**

```javascript
appmixer.api.on('error', error => {
  if (error.code === 401) {
    /* A request failed because the current access token is invalid ... */
  }
}
```

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

#### **`warning`**

```javascript
appmixer.api.on('warning', warning => { ... }
```

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


# UI & Widgets

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

## Configuration <a href="#configuration" id="configuration"></a>

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

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

#### `config.el` <a href="#config-el" id="config-el"></a>

Type: `String|Element` | Default: `null`

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

#### `config.theme` <a href="#config-theme" id="config-theme"></a>

Type: `Object` | Default: `DefaultTheme`

Custom theme definition.

#### `config.l10n` <a href="#config-l10n" id="config-l10n"></a>

Type: `Object` | Default: `DefaultL10N`

Custom localization texts.

#### `config.lang` <a href="#config-lang" id="config-lang"></a>

Type: `String` | Default: `en`

Language code for localization of components.

#### `config.api` <a href="#config-api" id="config-api"></a>

Type: `Object` | Default: `DefaultAPI`

Custom API methods.

## Instance <a href="#instance" id="instance"></a>

#### `widget.open` <a href="#widget-open" id="widget-open"></a>

```javascript
widget.open()
```

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

#### `widget.close` <a href="#widget-close" id="widget-close"></a>

```javascript
widget.close()
```

Unmount the `widget` instance and hide the `el` container.

#### `widget.reload` <a href="#widget-reload" id="widget-reload"></a>

```javascript
widget.reload()
```

Reload the entire `widget`.

#### `widget.reset` <a href="#widget-reset" id="widget-reset"></a>

```javascript
widget.reset()
```

Reset the state of the widget to defaults.

#### `widget.state` <a href="#widget-state" id="widget-state"></a>

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

```javascript
widget.state(path, value) // setter
widget.state(path) // getter
```

<details>

<summary>Example</summary>

```json
{
  "foo": false,
  "bar": { "counter": 1 }
}
```

```javascript
// 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 } }
```

</details>

#### `widget.set` <a href="#widget-set" id="widget-set"></a>

```javascript
widget.set(key, value)
```

Set `config` property.

#### `widget.get` <a href="#widget-get" id="widget-get"></a>

```javascript
widget.get(key, value)
```

Get `config` property.

#### `widget.on` <a href="#widget-on" id="widget-on"></a>

```javascript
widget.on(name, handler)
```

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

#### `widget.off` <a href="#widget-off" id="widget-off"></a>

```javascript
widget.off(name)
```

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


# Flow Manager

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

<figure><img src="/files/rmb81Aydr427qslF3nKJ" alt=""><figcaption><p>Flow Manager</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const flowManager = appmixer.ui.FlowManager(config)

flowManager.set(key, value)
flowManager.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

#### **`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:

```javascript
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' }
    ]
  }
}
```

{% hint style="info" %}
The *optional* `icon` property is a URL of an image or a `base64` string.
{% endhint %}

<figure><img src="/files/kqUXpYZB0Ves39ElAj4e" alt=""><figcaption><p>Flow Manager Menu</p></figcaption></figure>

#### **`config.options.shareTypes`**

Type: `Object` | Default: `DefaultShareTypes`

Override default sharing dialog types.

#### **`config.options.sharePermissions`**

Type: `Object[]` | Default: `DefaultSharePermissions`

Override default sharing dialog permissions.

#### **`config.options.filters`**

Type: `Object[]` | Default: `[]`

Create dropdown inputs with built-in query filters:

```javascript
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' }
    ]
  }
}
```

<figure><img src="/files/yRHUObkmuDdFlg8UHiQp" alt=""><figcaption><p>Flow Manager Filters</p></figcaption></figure>

#### **`config.options.customFilter`**

Type: `Object` | Default: `{}`

Filter the flows with additional parameters:

```javascript
appmixer.ui.FlowManager({
  /* ... */
  options: {
    customFilter: {
      stage: 'running'
    }
  }
}
```

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

```javascript
appmixer.ui.FlowManager({
  /* ... */
  options: {
    customFilter: {
      'customFields.category': 'healthcare',
      'customFields.template': true
    }
  }
}
```

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

```javascript
appmixer.ui.FlowManager({
    /* ... */
    options: {
        customFilter: {
            type: [                  // each flow has a 'type' property
                'automation',        
                'integration-instance'
            ]
        }
    }
});
```

#### **`config.options.sorting`**

Type: `Object[]` | Default: `[]`

Create dropdown inputs with built-in sorting:

```javascript
appmixer.ui.FlowManager({
  /* ... */
  options: {
    sorting: [
      { label: 'Last Modified', property: 'mtime', value: -1 },
      { label: 'Last Created', property: 'btime', value: -1 }
    ]
  }
}
```

<figure><img src="/files/MFF1zWBbKt0pz3JSB5n9" alt=""><figcaption><p>Flow Manager Sorting</p></figcaption></figure>

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
flowManager.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

#### **`layout`**

Type: `String` | Default: `grid`

Change layout of the widget.

#### **`query`**

Type: `Object` | Default: `DefaultQuery`

Set custom query parameters.

### Events <a href="#events" id="events"></a>

```javascript
flowManager.on(event, handler)
```

#### **`flow:open`**

```javascript
flowManager.on('flow:open', flowId => {/* ... */})
```

Select a flow to open in **Designer** widget.

#### **`flow:create`**

```javascript
flowManager.on('flow:create', () => {/* ... */})
```

Click **Create Flow** button.

#### **`flow:start`**

```javascript
flowManager.on('flow:start', flowId => {/* ... */})
```

Toggle flow stage button.

#### **`flow:stop`**

```javascript
flowManager.on('flow:stop', flowId => {/* ... */})
```

Toggle flow stage button.

#### **`flow:clone`**

```javascript
flowManager.on('flow:clone', flowId => {/* ... */})
```

Click menu item to clone a flow.

#### **`flow:share`**

```javascript
flowManager.on('flow:share', flowId => {/* ... */})
```

Click menu item to open sharing of a flow.

#### **`flow:rename`**

```javascript
flowManager.on('flow:rename', flowId => {/* ... */})
```

Click menu item to rename flow.

#### **`flow:remove`**

```
flowManager.on('flow:remove', flowId => {/* ... */})
```

Click menu item to remove a flow.

## Sharing <a href="#sharing" id="sharing"></a>

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

```javascript
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' }
    ]
  }
}
```

<figure><img src="/files/hc4nDaT7Y6ZGqczUxUY6" alt=""><figcaption><p>Flow Manager Sharing</p></figcaption></figure>

## Example

```javascript
// 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()
```


# Designer

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

<figure><img src="/files/8taYLX6wj1uBkzMNzu58" alt=""><figcaption><p>Designer</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const designer = appmixer.ui.Designer(config)

designer.set(key, value)
designer.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

#### **`config.flowId`**

Type: `String` | Default: `null`

ID of a flow that is opened in the editor.

#### **`config.componentId`**

Type: `String` | Default: `null`

ID of a component that is opened in the editor.

#### **`config.shareTypes`**

Type: `Object` | Default: `DefaultShareTypes`

Override default sharing dialog types.

#### **`config.sharePermissions`**

Type: `Object[]` | Default: `DefaultSharePermissions`

Override default sharing dialog permissions.

#### **`config.options.showHeader`**

Type: `Boolean` | Default: `true`

Toggle visibility of the header.\
\
\&#xNAN;**`config.options.validation`**

Type: `Object` | Default: `{}`

Controls validation panel settings.\
\
**Properties:**

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

**Example:**

```javascript
appmixer.ui.Designer({
  /* ... */
  options: {
    validation: { show: true }
  }
})
```

{% hint style="info" %}
Additional `validation` options may be added in future versions.
{% endhint %}

#### **`config.options.menu`**

Type: `Object[]` | Default: `[]`

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

```javascript
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' }
    ]
  }
})
```

{% hint style="info" %}
The *optional* `icon` property is a URL of an image or a `base64` string.
{% endhint %}

#### **`config.options.toolbar`**

Type: `Array[]` | Default: `[]`

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

```javascript
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()
                  }
              }
          }
        }]
    ]
  }
})
```

{% hint style="info" %}
Specify Vue [`ComponentOptions`](https://vuejs.org/api/component-instance.html#options) under `widget` to create a custom toolbar button.
{% endhint %}

#### **`config.options.autoOpenLogs`**

Type: `Boolean` | Default: `true`

Automatically open logs view when the flow is running.

```javascript
const designer = appmixer.ui.Designer({
  /* ... */
  options: {
      autoOpenLogs: true
      toolbar: [
        ['logs']
    ]
  }
})
```

#### **`config.options.triggerSelector`**

Type: `Object` | Default: `null`

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

```javascript
const designer = appmixer.ui.Designer({
  /* ... */
  options: {
    triggerSelector: {
      enabled: true,
      featured: [
        {
          name: 'appmixer.utils.timers.Timer'
        },
        {
          name: 'appmixer.utils.controls.OnStart',
          label: 'Custom label',
          description: 'Custom description',
          marker: 'Custom marker text',
          icon: 'data:image/svg+xml;base64,...',
        }
      ]
    }
  }
})
```

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
designer.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

**`stencilLayout`**

Type: `String` | Default: `'default'`

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

**`validationLayout`**

Type: `String` | Default: `'default'`

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

### Events <a href="#events" id="events"></a>

```javascript
designer.on(event, handler)
```

#### **`flow:start`**

```javascript
designer.on('flow:start', flow => {/* ... */})
```

Toggle stage button to start the flow.

#### **`flow:stop`**

```javascript
designer.on('flow:stop', flow => {/* ... */})
```

Toggle stage button to stop the flow.

#### **`flow:share`**

```javascript
designer.on('flow:share', flow => {/* ... */})
```

Click menu item to open sharing of the flow.

#### **`flow:rename`**

```javascript
designer.on('flow:rename', flow => {/* ... */})
```

Click menu item to rename the flow.

#### **`flow:export-svg`**

```javascript
designer.on('flow:export-svg', flow => {/* ... */})
```

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

#### **`flow:export-png`**

```javascript
designer.on('flow:export-png', flow => {/* ... */})
```

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

#### **`flow:print`**

```javascript
designer.on('flow:print', flow => {/* ... */})
```

Click menu item to print diagram of the flow.

#### **`flow:validation`**

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

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

#### **`flow:wizard-builder`**

```javascript
designer.on('flow:wizard-builder', flow => {/* ... */})
```

Click menu item to open a wizard builder dialog.

#### **`component:add`**

```javascript
designer.on('component:add', ({ data, next }) => {/* ... */})
```

Add a new component to the flow.

#### **`component:open`**

```javascript
designer.on('component:open', ({ data, next }) => {/* ... */})
```

Open component inspector.

#### **`component:close`**

```javascript
designer.on('component:close', ({ data, next }) => {/* ... */})
```

Close component inspector.

#### **`component:rename`**

```javascript
designer.on('component:rename', ({ data, next }) => {/* ... */})
```

Rename a component.

#### **`component:update-type`**

```javascript
designer.on('component:update-type', ({ data, next }) => {/* ... */})
```

Use selection input to change component type.

#### **`navigate:validation`**

```javascript
designer.on('navigate:validation', (flowId) => {/* ... */})
```

Click a button to show validation errors.

## Example

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


# Insights Logs

Browse logs of messages that passed through flows.

<figure><img src="/files/dX9cuns8m1MmlCA2NXfq" alt=""><figcaption><p>Insights Logs</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const insightsLogs = appmixer.ui.InsightsLogs(config)

insightsLogs.set(key, value)
insightsLogs.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

#### **`config.flowId`**

ID of a flow to filter the logs by.

#### **`config.options.showHistogram`**

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

```javascript
// Initialize with histogram hidden
const insightsLogs = appmixer.ui.InsightsLogs({
  options: { showHistogram: false }
});

// Later: show histogram dynamically
insightsLogs.set('options', {
  ...insightsLogs.get('options'), // keep other options unchanged
  showHistogram: true
});
```

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

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

```javascript
const insightsLogs = appmixer.ui.InsightsLogs({
    state: { filterLayout: 'collapsed' }
});

insightsLogs.state('filterLayout', 'expanded');
```

**`query`**

Type: `Object` | Default: `{}`

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

#### Basic Usage

Set an initial query in the widget's state:

```javascript
// Initial query: last 30 days
const insightsLogs = appmixer.ui.InsightsLogs({
  state: {
    query: {
      query: {
        range: {
          from: {
            endOf: null,
            startOf: 'day',
            subtract: [30, 'day']
          }
        }
      }
    }
  }
});
```

#### Reading and Updating the Query with Nested Filters

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

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

#### Tracking Query Changes

Listen for changes triggered by UI interactions:

```javascript
insightsLogs.on('change:query', queryAfterUserInput => {
  console.log(queryAfterUserInput);
});
```

#### Working with Nested Query Keys

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

```javascript
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
```

## Example

```javascript
const insightsLogs = appmixer.ui.InsightsLogs({
    el: '#insights-logs'
})

insightsLogs.open()
```


# Insights Chart Editor

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

<figure><img src="/files/tlNwOb8hbGs8H6bwfdjD" alt=""><figcaption><p>Insights Chart Editor</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const insightsChartEditor = appmixer.ui.InsightsChartEditor(config)

insightsChartEditor.set(key, value)
insightsChartEditor.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
insightsChartEditor.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

### Events <a href="#events" id="events"></a>

```javascript
insightsChartEditor.on(event, handler)
```

#### **`close`**

```javascript
insightsChartEditor.on('close', () => {/* ... */})
```

Close the editor.

## Example

```javascript
const insightsChartEditor = appmixer.ui.InsightsChartEditor({
    el: '#insights-chart-editor'
})

insightsChartEditor.open()
```


# Insights Dashboard

Browse and manipulate charts created by the current user.

<figure><img src="/files/hsXlaWok8z3mnmWurbEA" alt=""><figcaption><p>Insights Dashboard</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const insightsDashboard = appmixer.ui.InsightsDashboard(config)

insightsDashboard.set(key, value)
insightsDashboard.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
insightsDashboard.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

### Events <a href="#events" id="events"></a>

```javascript
insightsDashboard.on(event, handler)
```

#### **`chart:clone`**

```javascript
insightsDashboard.on('chart:clone', chartId => {/* ... */})
```

Clone chart.

#### **`chart:remove`**

```javascript
insightsDashboard.on('chart:remove', chartId => {/* ... */})
```

Remove chart.

#### **`chart:open`**

```javascript
insightsDashboard.on('chart:open', chartId => {/* ... */})
```

Open chart in **Chart Editor**.

## Example

```javascript
const insightsDashboard = appmixer.ui.InsightsDashboard({
    el: '#insights-dashboard'
})

insightsDashboard.open()
```


# Accounts

Manage accounts authorized by the current user.

<figure><img src="/files/LK03e1OeoVwY5SXqjUQN" alt=""><figcaption><p>Accounts</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const accounts = appmixer.ui.Accounts(config)

accounts.set(key, value)
accounts.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
accounts.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

### Events <a href="#events" id="events"></a>

```javascript
accounts.on(event, handler)
```

#### **`flow:open`**

```javascript
accounts.on('flow:open', flowId => {/* ... */})
```

Select a flow to open in **Designer** widget.

## Example

```javascript
const accounts = appmixer.ui.Accounts({
    el: '#accounts'
})

accounts.open()
```


# Storage

Manage records associated with data storage utility components of flows.

<figure><img src="/files/bBYD0asMxpppQrCmOCr1" alt=""><figcaption><p>Storage</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const storage = appmixer.ui.Storage(config)

storage.set(key, value)
storage.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

#### **`config.storeId`**

Type: `String` | Default: `[]`

ID of a store to open within the storage.

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
storage.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

## Example

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

storage.open()
```


# People Tasks

Manage tasks created by utility components of flows.

<figure><img src="/files/b4qMZoaOWxIbTD1CBPt7" alt=""><figcaption><p>People Tasks</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const peopleTasks = appmixer.ui.PeopleTasks(config)

peopleTasks.set(key, value)
peopleTasks.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
peopleTasks.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

## Example

```javascript
const peopleTasks = appmixer.ui.PeopleTasks({
    el: '#people-tasks'
})

peopleTasks.open()
```


# Connectors

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

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const connectors = appmixer.ui.Connectors(config)

connectors.set(key, value)
connectors.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
connectors.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

## Example

```javascript
const connectors = appmixer.ui.Connectors({
    el: '#connectors'
})

connectors.open()
```


# Integrations

Manage flows used as integration templates and instances.

<figure><img src="/files/svpIlTnZdX4j7vqwnk18" alt=""><figcaption><p>Integrations</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const integrations = appmixer.ui.Integrations(config)

integrations.set(key, value)
integrations.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

#### **`config.options`**

Type: `Object` | Default: `{}`

#### **`config.options.customFilter`**

Type: `Object` | Default: `{}`

Filter the integrations with additional parameters:

```javascript
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',
    }
  }
}
```

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

```javascript
appmixer.ui.Integrations({
    /* ... */
    options: {
        customFilter: [
            { 'customFields.category': 'your-category-for-templates' },
            { 'customFields.category': 'your-category-for-instances' }
        ]
    }
});
```

#### Updating the Custom Filter Dynamically

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

```javascript
// 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();
```

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
integrations.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

### Events <a href="#events" id="events"></a>

```javascript
integrations.on(event, handler)
```

#### **`integration:create`**

```javascript
integrations.on('integration:create', integrationId => {/* ... */})
```

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

#### **`integration:edit`**

```javascript
integrations.on('integration:edit', integrationId => {/* ... */})
```

Click a button to edit integration.

#### **`integration:remove`**

```javascript
integrations.on('integration:remove', integrationId => {/* ... */})
```

Click a button to remove integration.

#### **`integration:start`**

```javascript
integrations.on('integration:start', integrationId => {/* ... */})
```

Click a button to start integration.

#### **`integration:stop`**

```javascript
integrations.on('integration:stop', integrationId => {/* ... */})
```

Click a button to stop integration.

## Example

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


# Wizard

Manage a flow that is used as an integration instance.

<figure><img src="/files/Utdkyo1nksVEnNk6dvxY" alt=""><figcaption><p>Wizard</p></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const wizard = appmixer.ui.Wizard(config)

wizard.set(key, value)
wizard.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widgetconfig` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

#### **`config.flowId`**

Type: `String` | Default: `null`

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

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
wizard.state(name, value)
```

#### **`loader`**

Type: `Boolean` | Default: `null`

Toggle a custom loading state.

#### **`error`**

Type: `String` | Default: `null`

Toggle a custom error message.

### Events <a href="#events" id="events"></a>

```javascript
wizard.on(event, handler)
```

#### **`flow:start`**

```javascript
wizard.on('flow:start', flowId => {/* ... */})
```

Submit the form and start the flow.

#### **`flow:validation`**

```javascript
wizard.on('flow:validation', errors => {/* ... */})
```

Flow validation errors changed.

#### **`cancel`**

```javascript
wizard.on('cancel', () => {/* ... */})
```

Click a button to close the form.

#### `close`

```javascript
wizard.on('close', () => {/* ... */})
```

Submit the form and wait for the flow to start.

## Example

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


# Files

Manage files for use with components of flows.

<figure><img src="/files/w6YFtuJPasVd4bhfo6Y0" alt=""><figcaption></figcaption></figure>

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const files = appmixer.ui.Files(config)

files.set(key, value)
files.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

## Instance <a href="#state" id="state"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
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:**

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

### Events <a href="#events" id="events"></a>

```javascript
files.on(event, handler)
```

#### **`flow:open`**

```javascript
files.on('flow:open', flowId => {/* ... */})
```

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

## Example

```javascript
const files = appmixer.ui.Files({
    el: '#files'
})

files.open()
```


# Automation Hub

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

## Configuration <a href="#configuration" id="configuration"></a>

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

```javascript
const automationHub = appmixer.ui.AutomationHub(config)

automationHub.set(key, value)
automationHub.get(key)
```

#### **`config.el`** **`...`**

{% hint style="info" %}
Learn about `widget` `config` [here](/appmixer-ui-sdk/ui-and-widgets#configuration).
{% endhint %}

#### **`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.

#### **`config.options.header.visible`**

Type: `Boolean` | Default: `true`

Toggle visibility of the widget header.

#### **`config.options.header.tabs.hidden`**

Type: `String[]` | Default: `[]`

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

```javascript
appmixer.ui.AutomationHub({
  /* ... */
  options: {
    header: {
      tabs: {
        hidden: ['logs', 'accounts']
      }
    }
  }
})
```

#### **`config.options.flows.instances.tile.icons.size`**

Type: `String` | Default: `"M"`

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

#### **`config.options.flows.templates.tile.icons.size`**

Type: `String` | Default: `"M"`

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

#### **`config.state`**

Type: `Object` | Default: see below

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

```javascript
appmixer.ui.AutomationHub({
  /* ... */
  state: {
    tab: 'logs'
  }
})
```

## Instance <a href="#instance" id="instance"></a>

{% hint style="info" %}
Learn about `widget` instance [here](/appmixer-ui-sdk/ui-and-widgets#instance).
{% endhint %}

### State

```javascript
automationHub.state(name, value)
```

#### **`tab`**

Type: `String` | Default: `"flows"`

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

#### **`flows/layout`**

Type: `String` | Default: `"grid"`

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

#### **`flows/query/searchPattern`**

Type: `String` | Default: `""`

Text filter applied to the flows list.

#### **`flows/query/instances/onlyRunning`**

Type: `Boolean` | Default: `false`

When `true`, only running flow instances are shown.

### Events <a href="#events" id="events"></a>

```javascript
automationHub.on(event, handler)
```

{% hint style="info" %}
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.
{% endhint %}

#### **`flow:open-designer`**

```javascript
automationHub.on('flow:open-designer', ({ data }) => {/* ... */})
```

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.

#### **`flow:open-wizard`**

```javascript
automationHub.on('flow:open-wizard', ({ data }) => {/* ... */})
```

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.

#### **`flow:create-custom`**

```javascript
automationHub.on('flow:create-custom', ({ data, next }) => {/* ... */})
```

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

#### **`flow:start`**

```javascript
automationHub.on('flow:start', ({ data, next }) => {/* ... */})
```

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

#### **`flow:stop`**

```javascript
automationHub.on('flow:stop', ({ data, next }) => {/* ... */})
```

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

#### **`flow:clone`**

```javascript
automationHub.on('flow:clone', ({ data, next }) => {/* ... */})
```

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

#### **`flow:rename`**

```javascript
automationHub.on('flow:rename', ({ data, next }) => {/* ... */})
```

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

#### **`flow:remove`**

```javascript
automationHub.on('flow:remove', ({ data, next }) => {/* ... */})
```

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.

#### **`flow:insights-logs`**

```javascript
automationHub.on('flow:insights-logs', ({ data, next }) => {/* ... */})
```

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.

## Example

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


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

```javascript
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](/appmixer-ui-sdk/api-module#methods).

An example how to redefine the flow update request.

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


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

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

and/or use the option with individual widgets:

```javascript
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:

```javascript
// 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:

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

```javascript
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:

<table data-header-hidden><thead><tr><th width="161.2032967032967"></th><th></th><th width="175"></th><th></th></tr></thead><tbody><tr><td><code>action</code></td><td><code>action-vertical</code></td><td><code>action-dark</code></td><td><code>action-vertical-dark</code></td></tr><tr><td><code>trigger</code></td><td><code>trigger-vertical</code></td><td><code>trigger-dark</code></td><td><code>trigger-vertical-dark</code></td></tr><tr><td><code>selection</code></td><td><code>selection-vertical</code></td><td><code>selection-dark</code></td><td><code>selection-vertical-dark</code></td></tr></tbody></table>

### action/trigger

<figure><img src="/files/DVCS95kRjxseKN0qS09h" alt=""><figcaption></figcaption></figure>

### action-vertical/trigger-vertical

<figure><img src="/files/teyhevnw1PJ1NrFcNp2p" alt=""><figcaption></figcaption></figure>

### action-dark/trigger-dark

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

<figure><img src="/files/PzsyBk9Oo0Ppal3319Ly" alt=""><figcaption></figcaption></figure>

### action-vertical-dark/trigger-vertical-dark

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

<figure><img src="/files/gwaJRCSGYv4SD9g4l7ms" alt=""><figcaption></figcaption></figure>

## Charts

Charts (used in the Insights widgets - [Insights Logs](/appmixer-ui-sdk/ui-and-widgets/insights-logs), [Insights Chart Editor](/appmixer-ui-sdk/ui-and-widgets/insights-chart-editor) and [Insights Dashboard](/appmixer-ui-sdk/ui-and-widgets/insights-dashboard)) are customizable by a unique set of non-CSS properties. The values default to the current theme variables, except for `colorway`. The `colorway` option specifies the dynamic colors automatically picked by charts.

```javascript
appmixer.set('theme', {
    ui: {
        charts: {
            legendFontSize: "12px",
            legendFontFamily: "sans-serif",
            legendFontColor: "black",
            tickFontSize: "black",
            tickFontFamily: "monospaced",
            tickFontColor: "black",
            gridColor: "lightgray",
            colorway: [
                '#493843',
                '#61988E',
                '#A0B2A6',
                '#CBBFBB'
            ]
        }
    }
})
```

<figure><img src="/files/KtPeSgKj9MBbIJpTZteu" alt=""><figcaption></figcaption></figure>

## Advanced UI Styling

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

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

{% hint style="info" %}
While the advanced theme styling gives you the most flexibility in customizing Appmixer UIs, we cannot guarantee that in some cases, the structure and nesting of the selectors might change between Appmixer versions. It is therefore recommended to use [`theme.variables`](#variables) instead.
{% endhint %}

#### Colors

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

* `neutral96` is a foreground color with 96% opacity over the background `neutral00`.
* Some colors need a negative color `NG` on top. For example, a white text on a blue button.

#### Font

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

## Complete Theme Object

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

```bash
wget  https://my.appmixer.com/appmixer/package/theme-light.json
wget  https://my.appmixer.com/appmixer/package/theme-dark.json
```

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

![FlowManager Dark Theme](/files/-LtB50co5_IdusoqiTBc)

![Designer Dark Theme](/files/-LtB5YxKvoaXxvEkYEbI)

![Insights Logs Dark Theme](/files/-LtB6QxJ4i_uNuCrn51M)

![Insights Chart Editor](/files/-LtB6ee1vCRtxR5lYXhD)


# Custom Strings

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

## Setting Custom Strings

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

```javascript
var appmixer = new Appmixer({ baseUrl: BASE_URL });
appmixer.set('strings', STRINGS);
```

{% hint style="info" %}
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.
{% endhint %}

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

## Structure of the Strings Object

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

Example of setting the strings object:

```javascript
appmixer.set('strings', {
    ui: {
        flowManager: {
            search: 'Search flows',
            header: {
                buttonCreateFlow: 'Create new Flow'
            }
        }
    }
});
```

## Complete Strings Object

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

```bash
wget https://my.appmixer.com/appmixer/package/strings-en.json
```

## Time Localization

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

```javascript
appmixer.set('strings', {
  time: {
    months: [...],
    monthsShort: [...],
    weekDaysShort: [...],
    ordinal(number){ ... },
    relativeTime: {...}
  }
});
```

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

```javascript
ordinal(number) {
   const b = number % 10;
   const output = (~~(number % 100 / 10) === 1) ? 'th'
       : (b === 1) ? 'st'
           : (b === 2) ? 'nd'
               : (b === 3) ? 'rd' : 'th';
   return number + output;
}
```

## Pluralization and Strings with Variables

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

<figure><img src="/files/Y8AeZLHfP39TU3LTZ22t" alt=""><figcaption></figcaption></figure>

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:

```javascript
appmixer.set('strings', {
    ui: {
        flowManager: {
            pagination: '{{range}} of {{total}} flow|{{range}} of {{total}} flows'
        }
    }
});
```

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


# Custom Component Strings

Appmixer lets you manage the components' inspector fields through the manifest or the strings object.

There are two ways how to customize component's strings:

* Through a localization object inside the component's manifest
* Adding custom strings to components namespace inside strings object

## Component's manifest localization object

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

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

As you can see, there's a *localization* object at the end whose keys are language codes. This allows you to support multiple languages. Each value is an object whose keys are paths to the elements that will be customized (label, tooltip, placeholder, etc). The paths follow [JSON path](https://goessner.net/articles/JsonPath/) 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:

```javascript
// 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')
```

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

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

## Strings object's component namespace

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

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

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](https://goessner.net/articles/JsonPath/) syntax. For more information about the *Strings Object* refer to the [Custom Strings](/customizing-embedded-ui/custom-strings) section.

## service.json and module.json localization

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

### Example using localization object in service.json

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

### Example using Strings object

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

```json
"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ů."
}
```

## Application groups localization

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

![](/files/-M_5A53i62LtNC4o8CJa)

Use the localization strings to do so:

```javascript
appmixer.set('strings', {
    ui: {
        designer: {
            stencil: {
                groups: {
                    applications: 'Connectors',
                    utilities: 'Tools'
                }
            }
        }
    }
});
```

## Strings resolving

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

1. Localization object in the manifest (component.json).
2. Strings object components namespace.
3. Property in the manifest (component.json).

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


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

<figure><img src="/files/DXhTqAT3tG1oinN2g3vc" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/WQXzGRGAF1hW89rXQ7y9" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/bW5C2wJpWL5VzjqfDMcw" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/sGjnt3F4lwpIJRq007dM" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/f4veAxxxn8Kmh6b0YdRo" alt=""><figcaption></figcaption></figure>


# 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

<figure><img src="/files/1M5uMk5XElMCyFIhwg7W" alt=""><figcaption></figcaption></figure>

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

#### Applying Filters

1. Set your desired filter criteria using any combination of the options above
2. Click the **FILTER** button to apply your filters
3. Use **RESET** to clear all filters and return to the full log view
4. Click **RELOAD** to refresh the audit log data

#### Viewing Audit Log Details

The audit log table displays:

* **Action** - Description of the event that occurred
* **User** - The user who performed the action (displayed as email address)
* **Created** - Timestamp when the event occurred
* **Details icon** (eye icon) - Click to view additional details about the specific event including a diff table

### Logged Events

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

#### System Configuration

* Configuration Updated
* Configuration Deleted

#### Access Control

* ACL Rule Created/Updated
* Component Access Assigned

#### Flow Management

* Flow Created
* Flow Updated
* Flow Started/Stopped
* Flow Deleted
* Flow Cloned

#### Integration Template Management

* Integration Template Created
* Integration Template Updated
* Integration Template Published
* Published Integration Template Updated
* Integration Template Deleted

#### Integration Template Testing

* Integration Template Test Started
* Integration Template Test Stopped
* Integration Template Test Deleted

#### Connector Management

* Connector Installed
* Connector Updated
* Connector Uninstalled

#### User Management

* User Created
* User Updated
* User Deleted
* User Logged In
* Login Failed
* Password Changed

### Use Cases

#### Security Compliance

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

#### Troubleshooting

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

#### User Activity Monitoring

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

#### Change Tracking

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


# 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](/appmixer-backoffice/getting-started) 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.

<figure><img src="/files/3IUMvjqD77lfJ338UaKL" alt=""><figcaption></figcaption></figure>

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

<table><thead><tr><th width="227.37507552383653">Key</th><th width="306.7207646830474">Detail</th><th width="116.67578125">Default value</th><th data-type="checkbox">Needs restart</th></tr></thead><tbody><tr><td>API_USER_CREATE_SCOPE</td><td>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 <code>admin</code>.</td><td>null</td><td>false</td></tr><tr><td>APP_NAME</td><td>This will for example appear in the head title of a sign-in popup for Api Key services.</td><td>Appmixer</td><td>false</td></tr><tr><td>AUTH_HUB_AUTOMATIC</td><td>If the <em>auth-hub</em> system plugin is on and this value is <em>true,</em> 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 <em>clientId</em> and <em>clientSecret</em> the engine will use the Appmixer Authentication Hub for Slack authorization.</td><td>true</td><td>false</td></tr><tr><td>DEFAULT_USER_VENDOR</td><td>Vendor assigned to newly created users.</td><td><em>No value</em></td><td>false</td></tr><tr><td>AUTH_POPUP_DISPLAY_ERR</td><td>Whether to display <a href="/pages/-LATDlW_CYBb5fJ6HSRM#validate-function-object-or-string">validation errors</a> from the authentication modules.</td><td>true</td><td>false</td></tr><tr><td>AUTH_POPUP_TIMEOUT_ERR</td><td>How many seconds before automatically closing the <em>Connecting Account Failed</em> popup window.</td><td>5</td><td>false</td></tr><tr><td>BROKER_MESSAGE_ACK_TIMEOUT</td><td>Timeout for message processing.</td><td>1500000</td><td>false</td></tr><tr><td>COMPONENT_FACTORY_TIMEOUT</td><td>An attempt to create a component will fail after this timeout.</td><td>300000</td><td>false</td></tr><tr><td>COMPONENT_RECEIVE_TIMEOUT</td><td>A message will be retried if the receive() function does not return within this timeout.</td><td>1380000</td><td>false</td></tr><tr><td>LIMIT_FLOW_UPDATE_BYTES</td><td>The max size in bytes of a flow descriptor to be able to be saved.</td><td>2097152</td><td>false</td></tr><tr><td>LIMIT_CC_ARCHIVE_MAX_BYTES</td><td>Maximum size in bytes for custom components.</td><td>10485760</td><td>false</td></tr><tr><td>LIMIT_WEBHOOK_BYTES</td><td>Maximum payload size in bytes for webhook components.</td><td>1048576</td><td>false</td></tr><tr><td>WEBHOOK_REQUEST_TIMEOUT</td><td>Timeout in milliseconds for webhook component requests.</td><td>10000</td><td>false</td></tr><tr><td>LIMIT_COMPONENT_STATIC_CALL_MAX_BYTES</td><td>Maximum size in bytes of the payload for component static calls.</td><td>104857600</td><td>false</td></tr><tr><td>PUBLIC_FILES_PREFIX</td><td><a href="/pages/XHBkm08bOGnbHOUkWZ3s">Public files</a> (needed usually for domain verification) can be served from different paths. Path prefixes have to be separated by <code>:</code></td><td><pre><code>/:/.well-known
</code></pre></td><td>true</td></tr><tr><td>RETRY_BACKOFF</td><td>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.</td><td><pre><code>1,5,60,300,720
</code></pre></td><td>true</td></tr><tr><td>DISPATCHER_PREFETCH_COUNT</td><td>The maximum number of Rabbit messages being dispatched at the same time.</td><td>500</td><td>true</td></tr><tr><td>INPUT_QUEUE_PREFETCH_COUNT</td><td>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.</td><td>300</td><td>true</td></tr><tr><td>WEBHOOK_PREFETCH_COUNT</td><td>This is for webhooks from Appmixer to registered URLs. This is the amount of webhook messages that will be processing at a time.</td><td>50</td><td>true</td></tr><tr><td>WEBHOOK_RETRY_COUNT</td><td>Number of times that Appmixer will retry sending a webhook. Applies for all webhooks.</td><td>20</td><td>false</td></tr><tr><td>WEBHOOK_RETRY_INTERVAL</td><td>Initial interval in milliseconds for retries. Subsequent retries will take longer (multiplied by an internal factor).</td><td>30000</td><td>false</td></tr><tr><td>WEBHOOK_RETRY_MAX</td><td>Maximum interval in milliseconds between retries.</td><td>1800000</td><td>false</td></tr><tr><td>WEBHOOK_USER_CREATED</td><td>URL that will be called when new user is created (sign-up).</td><td><em>No value</em></td><td>false</td></tr><tr><td>WEBHOOK_FLOW_COMPONENT_ERROR</td><td>URL that will be called when a running flow encounters an error.</td><td><em>No value</em></td><td>false</td></tr><tr><td>WEBHOOK_FLOW_COMPONENT_ERROR_INCLUDE_QUOTA</td><td>Include quota errors among the errors sent to the registered webhook URL.</td><td>false</td><td>false</td></tr><tr><td>WEBHOOK_FLOW_COMPONENT_ERROR_INCLUDE_RETRY</td><td>Include <em>retry</em> attempts among the errors sent to the registered webhook URL. If <em>false</em>, the error will be sent, when all 30 attempts to process a message fail. If <em>true</em>, every failed attempt will be sent.</td><td>false</td><td>false</td></tr><tr><td>WEBHOOK_FLOW_STOPPED</td><td>URL that will be called when a flow is stopped due to an incompatible module upgrade.</td><td><em>No value</em></td><td>false</td></tr><tr><td>STRICT_COOKIES</td><td>If set to true, the engine will reject any incoming HTTP requests that have cookies that don't comply with the <a href="https://www.rfc-editor.org/rfc/rfc6265">HTTP cookies RFC specification</a>.</td><td>false</td><td>true</td></tr><tr><td>GARBAGE_COLLECTOR_CONTINUITY_SCOPES_TTL</td><td>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.</td><td>100</td><td>false</td></tr><tr><td>GC_FILES_ENABLED</td><td>If the Garbage collector for files is enabled.</td><td>true</td><td>true</td></tr><tr><td>GC_FILES_RULES</td><td>Rules for the Gargabge collector.</td><td>See below.</td><td>false</td></tr><tr><td>IDP_CONFIG</td><td>Identity provider configuration. Please see <a href="#configuring-single-sign-on">Configuring Single-Sing-On</a> for more informations.</td><td><em>No value</em></td><td>false</td></tr><tr><td>IDP_ROLES_PATH</td><td>Identity provider configuration. Please see <a href="#configuring-single-sign-on">Configuring Single-Sing-On</a> for more informations.</td><td><em>No value</em></td><td>false</td></tr><tr><td>IDP_DEFAULT_REDIRECT</td><td>Identity provider configuration. Please see <a href="#configuring-single-sign-on">Configuring Single-Sing-On</a> for more informations.</td><td><em>No value</em></td><td>false</td></tr></tbody></table>

GC\_FILES\_RULES

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

## Configuring Single Sign-On

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

**Required Configuration Parameters**

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

* **`idp_type`**: This specifies the authentication method. It can be either `oidc` or `saml`.
* **`authorization_url`**: This is the URL where users will be redirected to log in via your IdP.
* **`client_id`**: An identifier for your client application.

**Appmixer-Specific Parameters**

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

* **`domains`**: A list of domains for which you want to enable SSO. Any user with an email address from a domain listed here will be redirected to your IdP's authentication page. For example: `["yourcompany.com", "example.org"]`
* **`role_mapping`**: This object maps roles from your IdP to Appmixer's roles. Appmixer currently supports `admin` and `user` roles. For example, if your IdP uses "administrator" for admins and "member" for users, your mapping might look like: `{"admin": "administrator", "user": "member"}`

**Parameters Based on Authentication Type**

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

**SAML**

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

**OIDC**

* **`issuer`**: A unique string that identifies your identity provider.
* **`client_secret`**: A secret identifier for your client application.
* **`token_url`**: The URL where Appmixer can obtain a new OIDC token.
* **`metadata_url`**: A publicly accessible URL provided by your IdP that contains essential information for the authentication process.

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

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

{% tabs %}
{% tab title="OIDC" %}

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

{% endtab %}

{% tab title="SAML" %}

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

{% endtab %}
{% endtabs %}

**Applying Your Configuration**

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

<figure><img src="/files/bWFLcqDelzyZz9fNSylK" alt=""><figcaption></figcaption></figure>

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

<table><thead><tr><th width="257.2265625">Identity Provider</th><th>Roles Path</th></tr></thead><tbody><tr><td>Keycloak</td><td>realm_access.roles</td></tr><tr><td>Google ID</td><td><del><em>&#x3C;not supported></em></del></td></tr><tr><td>Google Workspace</td><td>depends on configuration</td></tr><tr><td>Amazon IAM</td><td>-</td></tr></tbody></table>

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

### Providers Configuration

#### Google Workspace

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

1. First, log in to your [Google Admin control panel](https://admin.google.com/)
2. In the menu on the left, select `Apps -> Web and Mobile Apps`\
   ![](/files/8jfFyBYY4Nvo7hwTLKKP)
3. Now, in the new tab, select `Add App -> Add Custom SAML app`\
   \
   .\
   ![](/files/UHEJtOEorHKs7DF7nPZ0)
4. Choose a name for your app and note all the details provided by Google; we will need them later.
5. On step 3 of the procedure, when prompted to provide the service provider details, please follow the screenshot, replacing `<your-tenant-url>` with the actual URL of your application.\
   ![](/files/Ou3AwVPlSKy9pcr6kZOO)
6. Leave everything blank on the last step `Attribute Mapping` for now.
7. Now that our SAML application is ready, we need to enable it for the users in our tenant. You can do it as you would for any other application (like Gmail, Drive, and so on).
8. Finally, let's set up the roles needed to log in to Appmixer.
9. Go to `Directory -> Users -> More options -> Manage custom attributes`\
   ![](/files/JE3FrH5K3BmAE4cISMuD)
10. On the next page, select `Add Custom Attribute` on the top right.\
    ![](/files/HsU1pTtbJD8dN6XOeynp)
11. Configure the attribute as shown in the screenshot. You can use any value you want for `Category` and `Description`. You can also change the name of your field.\
    ![](/files/yyfX9CNoFDNTKlq8koVQ)
12. Choose a user you want to assign roles to and expand the `Users Informations` tab.\
    ![](/files/8bJnM9byhsdJYB54efsu)
13. You will find the newly created attribute. If you scroll down, you can write any string here; they will have to match the content of the **`role_mapping`** in your configuration on Appmixer (Step 15).\
    ![](/files/91eR4zPPOFr9FortGlFw)
14. Now go back to the app we created earlier and edit the attribute mapping to expose the newly created attribute. If you decided to use `Roles` like in the example, you can follow the screenshot in this case too; otherwise, please replace the role in the left drop-down of the screenshot with the one you created before. Keep the value of `App attributes` to `Role` .\
    ![](/files/zxikgqn7ZzLJar1aBOL9)
15. When creating the configuration JSON for Appmixer, please use the following format:<br>

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

    \ <mark style="color:$warning;">(\*)</mark> <mark style="color:$warning;">**\<ENTITY ID>**</mark> <mark style="color:$warning;">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.</mark><br>
16. Please feel free to choose any value for `enforce_sso` and `disable_legacy_signup` and `auto_grant_user_role` based on your company policies.
17. Minify the JSON and add it to the Appmixer configuration under the name `IDP_CONFIG` .


# Getting Started

Appmixer command-line tool.

Use the Appmixer CLI tool to interact with the Appmixer engine remotely from the command line. It can be used to list flows, start/stop flows but more importantly, to **develop and publish custom components**.

### Installation

* Download and install NodeJS: <https://nodejs.org> (version 18 is required)
* `npm install -g appmixer`

### Help

Display the command options with the `-h` option:

```
$ appmixer -h
Usage: appmixer [options] [command]

Appmixer command line interface.

Options:
  -v, --version  output the version number
  -h, --help     output usage information

Commands:
  download|d     Download component.
  flow|f         Flow commands.
  init|i         Initialize component.
  login|l        Login into Appmixer API.
  logout|o       Logout from Appmixer API.
  pack|p         Pack component into archive.
  publish|pu     Publish component.
  remove|rm      Remove component.
  test|t         Test component, authentication module, ...
  url|u <url>    Set Appmixer API url.
  help [cmd]     display help for [cmd]

Go to https://docs.appmixer.com/appmixer/ to find more information.
```

Each command has its own help information:

```
$ appmixer flow -h
Usage: appmixer flow <command>

Flow commands.

Options:
  -h, --help         output usage information

Commands:
  start|s <flowId>   Start flow.
  stop|t <flowId>    Stop flow.
  remove|r <flowId>  Remove flow.
  ls|l [flowId]      Ls flow.
  help [cmd]         display help for [cmd]
```

### Initialization

First set the Appmixer API URL. This is the URL of your hosted Appmixer engine instance or your own custom URL where the self-managed engine is located. If you have a trial package or local installation, you can use <http://localhost:2200>.

```
$ appmixer url https://api.appmixer.com
```

Login to your Appmixer account and enter your password:

```
$ appmixer login david@client.io
prompt: password:

Login successful.
```

### Creating Custom Components

#### Generate a sample component

The best way to start implementing your own custom components is to use the generator tool to generate a sample component. This gives you the basic skeleton of your component/service that you can later tweak. Use the `appmixer init example` command to generate a sample component:

```
$ appmixer init example

Usage: appmixer init example <type> [path]

Options:
  --vendor [vendor]  Your vendor name.
  --verbosity        Verbosity level of the generated output log. Number in [0 - 2] range. Defaults to 1.
  -h, --help         output usage information

Examples:
  $ appmixer init example no-auth

Environment Variables:
AM_GENERATOR_COMPONENT_PATH		Components base directory. [path] argument overrides this variable if used.
```

As you can see, we have to give this command a `type` of example we want to generate. We will start with the most simple service type that does not use any authentication (OAuth1, OAuth2, API keys). In other words, the component will not ask the user to connect any account in the Inspector panel when using the component in a flow.

```
$ appmixer init example no-auth
Created directory structure ./appmixer/myservice/mymodule/MyComponent
Creating component appmixer.myservice.mymodule.MyComponent
Created component manifest file ./appmixer/myservice/mymodule/MyComponent/component.json.
Created component package file ./appmixer/myservice/mymodule/MyComponent/package.json.
Created component behaviour file ./appmixer/myservice/mymodule/MyComponent/MyComponent.js.
Created service manifest file ./appmixer/myservice/service.json.
Created quota module file ./appmixer/myservice/quota.js.

Example successfully generated.

my-components
└── appmixer
    └── myservice
        ├── mymodule
        │   └── MyComponent
        │       ├── MyComponent.js
        │       ├── component.json
        │       └── package.json
        ├── quota.js
        └── service.json

You can now use appmixer pack appmixer/myservice && appmixer publish commands to upload it.
```

The command prints all the generated files and the directory structure. Note that we have just generated a working component with the `myservice.mymodule.MyComponent` type.

#### Pack your component

Now we're ready to pack our service (i.e. create a zip archive with all the generated files) using the `appmixer pack appmixer/myservice`command:

```
$ appmixer pack appmixer/myservice
Packing component directory: /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice

Files found in /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice
- mymodule
- quota.js
- service.json

You are in a directory with service.json file.
I'm going to create directory structure based on name in your service.json file.


3866 total bytes
appmixer.myservice.zip
```

{% hint style="info" %}
You can pack the entire service or just the module or even individual components by providing the path to the `appmixer pack` command.
{% endhint %}

The `pack` command generated the `appmixer.myservice.zip` file in the current directory.

#### Publish your component

{% hint style="info" %}
Before you publish a component. Your user account has to have property `vendor` set to a string or an array of strings. The value depends on what `vendor` is used in component(s) you're about to publish. More about that in [here](broken://pages/-LATDlWj1py98gKglNj8#directory-structure-and-files). In the examples in this section we use `appmixer` as a `vendor`, but you should use your own. If your company is called `acme` then the `vendor` property should be set to `acme` as well. You can use Backoffice for that as shown in the next picture.
{% endhint %}

![Using Backoffice to set vendor property.](/files/-MW3w1SGYXWWAoHPv62D)

The last step is to publish our component for our users to use. This is done using the `appmixer publish` command:

```
$ appmixer publish appmixer.myservice.zip
Publishing archive: /Users/daviddurman/Projects/appmixer/my-components/appmixer.myservice.zip
Published.
```

Our component is now published and ready to be used:

![Custom component published](/files/-LrJuoeDwuRxhsgTQWfU)

![](/files/-LrJwfoipJ-HlKFLzbyq)

Notice the labels and icons of the service in the component panel on the left and of the component in the Inspector selector match our definitions from the `myservice/service.json` and `myservice/mymodule/MyComponent/component.json` manifest files.

#### List all available components

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

```
$ appmixer component ls
appmixer.actimo.contacts.CreateContact
appmixer.actimo.contacts.DeleteContact
appmixer.actimo.contacts.GetContact
appmixer.actimo.contacts.GetContacts
appmixer.actimo.contacts.UpdateContact
appmixer.actimo.groups.GetGroups
appmixer.actimo.messages.SendMessage
appmixer.apify.crawlers.Crawl
appmixer.asana.projects.CreateProject
appmixer.asana.projects.NewProject
appmixer.asana.tasks.CreateStory
appmixer.asana.tasks.CreateSubtask
appmixer.asana.tasks.CreateTask
appmixer.asana.tasks.NewComment
...
```

#### Remove your component

If you decide your component is no longer needed, you can remove your component from the system with the `appmixer remove` command:

```
appmixer remove appmixer.myservice.mymodule.MyComponent
```

The remove command lets you specify any portion of the fully qualified component type. For example, if you want to remove the entire service, you can do that with:

```
appmixer remove appmixer.myservice
```

{% hint style="info" %}
It's important to note that removing a component that is used in any of the running flows will cause the flows to stop working. The flows will start generating errors that will be visible in the Insights section. The `remove` command does not automatically stop the flows. Always make sure the component is not used in any of the running flows before you remove it.\
\
IMPORTANT: Removing a component cannot be undone!
{% endhint %}

#### Updating your component

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

{% hint style="info" %}
Note that if you "dramatically" change your component, e.g. removing ports, flows using the component type might require re-configuration since the ports that were used to connect to other components will not exist. The same goes for other major changes: changing output parameters that are used in connected components, changing inspector config and properties, etc.
{% endhint %}

#### Testing your component

{% hint style="warning" %}
If want to run the next commands on Windows, make sure you escape the JSONs correctly. For example, instead of

`appmixer test component HelloAppmixer -i '{"in": {"text": "abc"}}'`

you have to use

`appmixer test component HelloAppmixer -i "{\"in\": {\"text\": \"abc\"}}"`
{% endhint %}

Writing/updating code of your component, re-publishing it and re-configuring/restarting your flow every time you need to test your component would be a long process. Therefore, the Appmixer CLI tool provides a command to test your component locally on your machine, before publishing it to Appmixer. The `appmixer test` command allows you to test your component by sending messages to it, configuring properties and testing authentication methods:

```
$ appmixer test -h
Usage: appmixer test <command>

Dev tools for testing your files.

Options:
  -h, --help                   output usage information

Commands:
  dump|d <moduleName>          Get stored authentication data from previous commands.
  auth|a <authModuleFile>      Authenticate service.
  component|c <componentFile>  Test component.
  help [cmd]                   display help for [cmd]
```

We'll start by exploring the component testing tool:

```
$ appmixer test component -h
Usage: appmixer test component [options] [componentDir]

Options:
  -f, --transform [transform]    specify transformer
  -i, --input [input]            input test message object (default: [])
  -m, --mime [mime]              mime type, application/json by default
  -p, --properties [properties]  component properties (JSON format)
  -s, --no-state                 do not show component's state
  -t, --tickPeriod [tickPeriod]  tick period (in ms), default is 10000 ms
  -h, --help                     output usage information

Examples:
  Following example will send input message { "to": "your@email.com" } to component's input port 'in'.
  You always have to specify to which input port you want to send message.
  $ appmixer test component [path-to-your-component-directory] -i '{ "in": { "to": "your@email.com" } }'

  This is how to specify transformer function from transformer file.
  $ appmixer test component [path-to-component] -i '{}' -f './transformers#channelsToSelectArray'

  How to set properties and tick period:
  $ appmixer t c [path-to-component] -p '{ "channelId: "123XYZ" }' -t 2000

  You can send more than one message:
  $ appmixer t c [path-to-component] -i '{ "in": { "to": "first@email.com" }}' -i '{ "in": { "to": "second@email.com" }}'

  You can run appmixer command in your component's directory:
  $ appmixer test c
  Directory has to contain component.json file and component's source code file.

If you're developing component that needs authentication, use 'appmixer test auth' before.
If you're developing Oauth2 component you might need to refresh access token before calling this command.
Use 'appmixer test auth refresh' for such purposes.

Some of the feature (context.store, context.componentStaticCall, ... will work only if you are logged in into Appmixer.
If you want to test them in your component, call appmixer login first.

For more information, checkout our documentation at:
https://docs.appmixer.com/appmixer/component-definition/authentication
https://docs.appmixer.com/appmixer/appmixer-trial/custom-component-helloappmixer
```

Let's say we want to send a message to our component and see how it reacts, i.e. what is the output of our component. We know our component has an input port called `in` and requires the `sourceData` property as part of the incoming messages (see the `component.json` file of your MyComponent, especially the `inPorts` section). We can use the `test` command for this:

```
$ appmixer test component appmixer/myservice/mymodule/MyComponent -i '{ "in": { "sourceData": "foo" } }'

Testing /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice/mymodule/MyComponent

Validating properties.

Test server is listening on 2300

Starting component.

Calling receive method with input message:
in:
  -
    properties:
      correlationId:     null
      gridInstanceId:    null
      contentType:       application/json
      contentEncoding:   utf8
      sender:            null
      destination:       null
      correlationInPort: null
      componentHeaders:
      signal:            false
    content:
      sourceData: foo
    scope:
{"name":"component","hostname":"MacBook-Pro.local","pid":26397,"level":30,"msg":"{\"properties\":{\"correlationId\":\"8f2ce09e-3f6d-48dd-81bd-e80189f70bb4\",\"gridInstanceId\":null,\"contentType\":\"application/json\",\"contentEncoding\":\"utf8\",\"sender\":{\"componentId\":\"70eb49e9-88df-4d0e-9549-d4e4f0f755c0\",\"type\":\"appmixer.myservice.mymodule.MyComponent\",\"outputPort\":\"out\"},\"destination\":null,\"correlationInPort\":null,\"componentHeaders\":{},\"signal\":false},\"content\":{\"sourceData\":\"foo\"},\"scope\":{\"_walkthrough\":[{\"targetId\":\"70eb49e9-88df-4d0e-9549-d4e4f0f755c0\",\"links\":[]}]}} { componentId: '70eb49e9-88df-4d0e-9549-d4e4f0f755c0',\n  flowId: 'c5d05118-13b5-4ec8-a8fe-2e6ef51fdd52',\n  userId: '5da735715abc4a671dfb592f',\n  componentType: 'appmixer.myservice.mymodule.MyComponent',\n  type: 'data',\n  portType: 'out',\n  port: 'out',\n  inputMessages: { in: [ [Object] ] },\n  annotatedMsg: { sourceData: 'foo' } }","time":"2019-10-16T15:21:22.169Z","v":0}

Component sent a message to its output port: out
{ sourceData: 'foo' }

Component's receive method finished in: 45 ms.

Return value from receive method:
undefined

Component's state at the end:
State is empty, component did not store anything into state.

Stopping component.

Destroying component.
```

The command prints out a lot of useful information, for example, the output of the component (see *"Component sent a message to its output port: out"*). Since our component just forwards the same data that it received, we see the same object we sent to it: *{ sourceData: 'foo' }*.

Now suppose we have a bug in our code in MyComponent.js, a syntax error:

```
module.exports = {
    receive(context) {
        myBadError
        context.sendJson(context.messages.in.content, 'out');
    }
}
```

Now re-running the test gives us:

```
$ appmixer test component appmixer/myservice/mymodule/MyComponent -i '{ "in": { "sourceData": "foo" } }'

Testing /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice/mymodule/MyComponent

Validating properties.

Test server is listening on 2300

Starting component.

Calling receive method with input message:
in:
  -
    properties:
      correlationId:     null
      gridInstanceId:    null
      contentType:       application/json
      contentEncoding:   utf8
      sender:            null
      destination:       null
      correlationInPort: null
      componentHeaders:
      signal:            false
    content:
      sourceData: foo
    scope:

[ERROR]: myBadError is not defined
```

#### Downloading your component

When your component is published, you can always download it back to your local file system. To download the source code of your component, use the `appmixer download` command:

```
$ appmixer download -h
Usage: appmixer download selector

Options:
  -o, --out [dir]  Where you want save it.
  -h, --help       output usage information

Examples:
  Download all files for SendEmail component:
  $ appmixer download vendor.google.gmail.SendEmail

  Download only package.json file for SendEmail component:
  $ appmixer download vendor.google.gmail.SendEmail/package.json

  Download main SendEmail.js file only:
  $ appmixer download vendor.google.gmail.SendEmail/SendEmail.js

  Download all gmail components:
  $ appmixer download vendor.google.gmail

  Download auth.js file for gmail:
  $ appmixer download vendor.google.gmail/auth.js

  Download quota.js file for gmail:
  $ appmixer download vendor.google.gmail/quota.js
```

In our case, the download command would look like:

```
$ appmixer download appmixer.myservice
$ ls
appmixer.zip
$ unzip appmixer.zip
$ tree appmixer/
appmixer
└── myservice
    ├── mymodule
    │   └── MyComponent
    │       ├── MyComponent.compiled.js
    │       ├── MyComponent.js
    │       ├── component.json
    │       ├── package-lock.json
    │       ├── package.json
    │       └── sourcemap-register.js
    └── service.json

3 directories, 7 files
```

### Working With Flows

#### Listing Flows

You can list all your flows using the `appmixer flow ls` command:

```
$ appmixer flow ls
[Get Current Weather] : [a5769b32-8835-44ad-82e1-ece2874ea3e3] : [stopped]
[Uptime Monitor] : [5b5fd3a0-0ef2-4fc5-9a60-164f5e44c660] : [stopped]
[Daily Rainy Day Alert] : [ec1103e5-c66c-41c4-9223-029d2b328f5c] : [stopped]
```

If you want to see just one flow and its stage (running/stopped), you can pass the ID of the flow in the `flow ls` command:

```
$ appmixer flow ls a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow: a5769b32-8835-44ad-82e1-ece2874ea3e3
Stage: stopped
```

To see the flow descriptor of a flow (i.e. JSON object that represents the entire configuration of the components in the flow and their connections), add the `--descriptor` or `-d` flag:

```
$ appmixer flow ls a5769b32-8835-44ad-82e1-ece2874ea3e3 --descriptor
{
    "5ba2740c-929b-4599-b09e-fc8f8d5dac82": {
        "type": "appmixer.utils.controls.OnStart",
        "label": "OnStart",
        "source": {},
        "config": {},
        "x": 88,
        "y": 110
    },
    "0f366972-08fe-4cd4-80c0-227cfb6db54b": {
        "type": "appmixer.utils.weather.GetCurrentWeather",
        "label": "GetCurrentWeather",
        "source": {
            "location": {
                "5ba2740c-929b-4599-b09e-fc8f8d5dac82": [
                    "out"
                ]
            }
        },
        "config": {
            "transform": {
                "location": {
                    "5ba2740c-929b-4599-b09e-fc8f8d5dac82": {
                        "out": {
                            "type": "json2new",
                            "lambda": {
                                "city": "Prague",
                                "units": "metric"
                            }
                        }
                    }
                }
            }
        },
        "x": 286,
        "y": 110
    },
    "ab6a22f8-916d-4aab-a5e3-2abedc03917c": {
        "type": "appmixer.utils.email.SendEmail",
        "label": "SendEmail",
        "source": {
            "in": {
                "0f366972-08fe-4cd4-80c0-227cfb6db54b": [
                    "weather"
                ]
            }
        },
        "config": {
            "transform": {
                "in": {
                    "0f366972-08fe-4cd4-80c0-227cfb6db54b": {
                        "weather": {
                            "type": "json2new",
                            "lambda": {
                                "from_email": "info@appmixer.com",
                                "subject": "Appmixer: Current Weather",
                                "text": "Temperature: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.temp}}} dgC\nPressure: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.pressure}}} hPa\nHumidity: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.humidity}}}%\nCloudiness: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.clouds.all}}}%",
                                "to": ""
                            }
                        }
                    }
                }
            }
        },
        "x": 484,
        "y": 110
    }
}  
```

#### Starting and Stopping Flows

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

```
$ appmixer flow start a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow a5769b32-8835-44ad-82e1-ece2874ea3e3 successfully started.

$ appmixer flow stop a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow a5769b32-8835-44ad-82e1-ece2874ea3e3 successfully stopped.
```

#### Removing Flows

To remove flows, use the `appmixer flow remove` command:

```
$ appmixer flow remove 2058a1ee-9c19-4e94-bd7a-0da7f9bed973
Flow 2058a1ee-9c19-4e94-bd7a-0da7f9bed973 successfully removed.
```

{% hint style="info" %}
Note that the removed flow will be automatically stopped if it was running. Also, note that this action cannot be undone.
{% endhint %}

### Working with Modifiers

You need admin privileges to run these commands. You should not modify Modifiers if there are any running flows using them. Such an operation would break the flows.

#### Downloading Modifiers

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

```
$ appmixer modifiers get
```

#### Publishing Modifiers

The next command will publish modifiers into Appmixer.

```
$ appmixer modifiers publish file-with-your-modifiers.json
```

#### Deleting Modifiers

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

```
$ appmixer modifiers delete
```


# OpenAPI Connector Generator

The Appmixer OpenAPI generator provides a tool to automatically generate Appmixer components from the OpenAPI v3 specification.

The tool generates an Appmixer connector from an OpenAPI specification. The spec can be extended with special `x-connector-...` extensions to either provide details that the OpenAPI spec does not define (but are necessary for the Appmixer connector) or to make the resulting connector more user friendly (automatic pagination, dynamic select with options instead of providing hardcoded values, ...).

### Getting started

In the simplest form, you can run the generator by passing an OpenAPI spec as a parameter (both *.json* and *.yaml* file formats are accepted) together with a directory where the Appmixer connector will be generated:

```
$ appmixer init openapi ./examples/openapi/boredapi/openapi.json ./examples/openapi/boredapi/
```

Outputs:

```
OpenAPI specification is valid.
No `security` scheme defined in the root level. Authentication module will not be generated.
API name: Bored API, Version: 1.0.0
Statistics: 1 Operations, 1 Paths, 0 Webhooks, Generated Components 1
Auth scheme used: null
Used extensions:
```

The resulting connector is in the output directory:

```
$ tree ./examples/openapi/boredapi

boredapi/
├── bundle.json
├── core
│   └── GetActivity
│       ├── GetActivity.js
│       ├── component.json
│       └── package.json
├── openapi.json
├── package.json
└── service.json
```

This generates a complete Appmixer connector that can be packed and published to an Appmixer tenant:

```
$ appmixer pack ./examples/openapi/boredapi
$ appmixer publish appmixer.boredapi.zip

```

### Patching the OpenAPI specification

If you use a 3rd party OpenAPI specification and want to make changes to it or enrich it with extensions, it would not be practical to edit this JSON/YAML file directly. Editing the file directly would make it hard to keep track of changes or update the original spec file when needed. To avoid direct editing of the spec, the Appmixer OpenAPI generator accepts a separate JSON Patch file (.json-patch) that defines changes to the OpenAPI spec. This way, the original OpenAPI spec can be left intact and only changes to it (together with extensions) can be defined separately.

The *JSON Patch* file follows the [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902/) ([a quick reference](https://jsonpatch.com/)) with some small modifications to make it easier to identify values that need to be changed in the OpenAPI spec:

* The standard *JSON Patch* file uses [JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901/) to identify values in a JSON file. However, the JSON Pointer format does not provide the expressive power that one would expect from a format that is to be manually written, especially when identifying multiple places within a JSON file using a single expression.

Consider the following example that removes the `clientId` parameter from all the `parameters` in the OpenAPI spec because the `clientId` is already part of the authentication screens. In other words, we don't need this parameter to be part of the configuration of each Appmixer component (Inspector UI) since we got the value once when the user authenticated to the connector (created a Connection). Since our example OpenAPI spec includes the `clientId` parameter in all the `paramters` section for each path item, we want to remove all the occurences. If we were to use the standard JSON Pointer format, we would have to list all the occurences of the `clientId` inside the `parameters` section for each path item such as:

```
{ "op": "remove", "path": "/paths/~1service~1tickets/parameters/2" },
{ "op": "remove", "path": "/paths/~1company~1contacts/parameters/1" },
{ "op": "remove", "path": "/paths/~1user~1emails/parameters/2" },
...
```

As you can see, this is not exactly user friendly. Instead, the Appmixer OpenAPI generator introduces the `jsonpath` parameter for operations that accept the JSON Pointer-based `path` parameter and therefore \[]allows us to use a single `remove` operation instead:

```
{ "op": "remove", "jsonpath": "$..parameters[?(@.name==\"clientId\" && @.in == \"header\")]" }
```

The `jsonpath` parameter accepts a [JSON Path expression](https://goessner.net/articles/JsonPath/). The Appmixer OpenAPI generator pre-processes the JSN Patch file to expand operations that contain the `jsonpath` parameter by finding all the values in the JSON OpenAPI spec document that the JSON Path expression points to and multiplying the operation for each value found while replacing the `jsonpath` with `path` containing the JSON Pointer expression that identifies the value.

To patch the OpenAPI spec with a JSON Patch file, pass the path to the patch file to the `--patch` argument:

```
$ appmixer init openapi ./examples/openapi/zoom/ZoomMeetingAPISpec.json --patch ./examples/openapi/zoom/openapi.json-patch ./zoom
```

### Artifacts

Sometimes, it is useful to be able to see byproducts of the preprocessing of the OpenAPI spec and the patch file. To generate these byproducts, use the `--artifacts` argument:

```
$ appmixer init openapi ./examples/openapi/zoom/ZoomMeetingAPISpec.json --patch ./examples/openapi/zoom/openapi.json-patch --artifacts ./zoom
```

This will create a special directory `artifacts/` under the output directory with the following files:

```
$ tree zoom/artifacts/

zoom/artifacts/
├── checksum.json
├── openapi.json
├── openapi.normalized.json-patch
├── openapi.original.json-patch
└── openapi.patched.json

0 directories, 5 files
```

* `checksum.json` contains a SHA-256 digests for all the files generated by the generator. This is useful for a quick check whether manual changes have been made to any of the generated files or whether a re-generation of the connector produced a different result (possibly due to changes in the OpenAPI spec).
* `openapi.json` is the original OpenAPI spec file.
* `openapi.normalized.json-patch` is the JSON Patch file withe "normalized" `jsonpath` attributes. See Patching OpenAPI specification.
* `openapi.original.json-patch` is the original JSON Patch file.
* `openapi.patched.json` is the final OpenAPI spec, normalized, dereferenced (i.e. with resolved `$ref` references) and possibly patched.

### Array vs Single Object Outputs

The Appmixer OpenAPI generator automatically checks for the operation success response JSON schema and if it detects `type: array`, it adds an "Output Options" select box to the resulting generated component configuration. This allows the end-user to select, whether they are interested in outputting all items at once (array output) or one item at a time (object output). The same logic applies when the `x-connector-pagination` extension is defined on the operation.

### OpenAPI specification to Appmixer Connector mapping

The following table describes how the OpenAPI specification fields/features are mapped to Appmixer connectors.

| OpenAPI field/feature  | Appmixer Connector Field/Feature                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `info.title`           | **Service name** (lower cased with spaces removed). Can be customized using `x-connector-service`                                                                                                                                                                                                                                                                                                                                                                                          |
| `info.description`     | **Service description**                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `info.title`           | Bundle changelog version description                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `servers[0].url`       | The first server is used as the **base URL** for all HTTP calls in the connector. Other servers are ignored.                                                                                                                                                                                                                                                                                                                                                                               |
| `servers[0].variables` | For `apiKey` based **authentication** schemes, server variables are requested by the end-user in the authentication flow (i.e. considered in the `auth.js` form and later used to construct the base URL). Only the first server variables are used. Other servers are ignored.                                                                                                                                                                                                            |
| `paths`                | Each *Operation Object* is converted to an Appmixer **component**. The path of the *Path Object* is used to construct the URL to be called (using the HTTP method from the *Path Item Object*) by the component as part of its behaviour.                                                                                                                                                                                                                                                  |
| `paths...operationId`  | The operation ID is used as the Appmixer **component name** after normalization. The normalization consists of camel casing the operation ID if it contains the `.` (dot) character since the dot character can't be used in the Appmixer component name. Otherwise, the operation ID is used as is. if the operation ID is missing in the OpenAPI spec, the Appmixer component name is constructed from the HTTP method and path (e.g. `GET /user`) becomes `GetUser`.                    |
| `paths...responses`    | The first JSON-like success response object is heuristically found and used to construct the **output of the component**. Only one `out` output port is always created and the response JSON schema is used as to construct the `options` object of the output port (including JSON schemas for nested objects and arrays). The heuristics to find the first JSON-like response object finds the first response with `2xx` HTTP Status Code and Media Type containing the `"json"` string. |
| `paths...requestBody`  | The request body object is used to construct the **input of the component** (together with `parameters`). Only the following media types are supported: `applicatoin/json`, `application/x-www-form-urlencoded` and `multipart/form-data`. The input of the component is flattened and all nested properties of the original request body are delimited using the \`'                                                                                                                      |
| `paths...parameters`   | The parameters are used to construct the **input of the component** (together with `requestBody`).                                                                                                                                                                                                                                                                                                                                                                                         |

### File Uploads

Input fields that are of the JSON schema type `string` and format `binary` are considered binary file inputs. These input fields are mapped to the Appmixer `filepicker` inspector field type. For such inputs, the Appmixer OpenAPI generator automatically generates component code that loads the file from the Appmixer Files storage and streams it to the API endpoint. The `multipart-form-data; boundary=FORM_BOUNDARY` `Content-Type` HTTP header is automtically set on these requests.

### Selecting Operations for Conversion

When using an external OpenAPI spec that you do not want to edit, use the JSON Patch with the following operation to select OpenAPI operations that you want to convert to Appmixer components:

```
{ "op": "remove", "jsonpath": "$.paths.*[?(@.operationId != 'meetings' && @.operationId != 'meetingCreate')]" }
```

In the example above, only the `meetings` and `meetingCreate` operations will be considered by the OpenAPI generator for conversion.

### Supported Authentication Types

Only OAuth 2 and API key based (with keys in all query/header/cookies) authentication is supported. The generated authentication module can be controlled with the `x-connector-connection`, `x-connector-connection-check` and `x-connector-connection-profile` extensions.

### OpenAPI Extensions

The Appmixer OpenAPI generator accepts multiple extensions that can be used to define constructs that the standard OpenAPI specification does not provide. Without these constructs, the final generated connectors would not be as user friendly.

#### x-connector-version

**Description**

Define the version of the connector. The connector is versioned using a `major.minor.patch` versioning scheme. The `major.minor` portion of the version string (for example `3.1`) shall designate the connector feature set. `.patch` versions address errors in, or provide clarifications to, the connector, not the feature set.

*Note that, unfortunately, we can't use `info.version` as the connector version since `info.version` can be an abitrary string while the connector REQUIRES the use of semantic versioning.*

**Location**

* Info Object

**Value**

A version string using the `major.minor.patch` versioning scheme. If not provided, the default value is `1.0.0`.

**Example**

```
"info": {
  ...
  "x-connector-version": "2.1.1"
  ...
}
```

#### x-connector-icon

**Description**

Define an icon for the connector.

**Location**

* Info Object

**Value**

A URL to an image (png, jpeg or SVG). A Data URI is also accepted.

**Example**

```
"info": {
  ...
  "x-connector-icon": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
  ...
}
```

#### x-logo

**Description**

Alternatively to the `x-connector-icon`, the Appmixer OpenAPI generator also accepts the more common `x-logo` extension.

**Location**

* Info Object

**Value**

Either a URL to an image (png, jpeg or SVG; with data URI also accepted) or an object with a `url` field that points to an image.

**Example (URL directly)**

```
"info": {
  ...
  "x-logo": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
  ...
}
```

**Example (nested url field)**

```
"info": {
  ...
  "x-logo": {
      "url": "https://www.jotform.com/resources/assets/svg/jotform-icon-white.svg"
      ...
  }
  ...
}
```

#### x-connector-service

**Description**

Define a name for the Appmixer connector service of the service/module/component hierarchy.

**Location**

* Info Object

**Value**

A string. If not provided, the default value is the `info.title` in lower case with removed spaces.

**Example**

```
"info": {
  ...
  "x-connector-service": "jotform"
  ...
}
```

#### x-connector-module

**Description**

Define a name for the Appmixer connector module of the service/module/component hierarchy.

**Location**

* Info Object

**Value**

A string. Deault value is `core`.

**Example**

```
"info": {
  ...
  "x-connector-module": "forms"
  ...
}
```

#### x-connector-connection-check

**Description**

An HTTP request that will be called to validate whether the user authentication credentials (Connection) are valid. In the most common sense, this request is called to check whether the api key or OAuth access token is valid. Typically, this is a call to a `/me`-type of endpoint that is sent with authentication details included (OAuth access token, api key, ...). The check is valid if the response is a success (HTTP 2xx status code).

**Location**

* Security Scheme Object

**Parameters**

* `method` ... HTTP method (GET, POST, ...)
* `url` ... The endpoint to be called. If the URL is relative, it will be relative to the base URL (defined in the OpenAPI `servers` section).
* `headers` ... HTTP headers object.
* `query` ... HTTP query object (key-value pairs representing the query string parameters).

The values of the `url`, `headers`, and `query` parameters can contain parameters enclosed in curly braces. These parameters will be replaced by the values collected from the user during the authentication flow (the named parameters in the Security Scheme Object) or are implicitely provided (`accessToken` in case of OAuth).

**Examples**

**A common use in API keys-based authentication:**

```
"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-check": {
                "method": "GET",
                "url": "/user?apiKey={apiKey}"
            },
            ...
        }
    },
```

**A common use in Oauth-based authentication:**

```
"components": {
    "securitySchemes": {
      "OAuth": {
        "x-connector-connection-check": {
          "method": "GET",
          "url": "/users/me",
          "headers": {
              "Authorization": "Bearer {accessToken}"
          }
        },
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://zoom.us/oauth/authorize",
            "scopes": {},
            "tokenUrl": "https://zoom.us/oauth/token"
          }
        },
        "type": "oauth2"
      }
    }
}
```

#### x-connector-connection-profile

**Description**

An HTTP request that will be called to get the user profile (used mainly to get a display name for the Connection). Typically, this is a call to a `/me`-type of endpoint that is sent with authentication details included (OAuth access token, api key, ...). The returned value is then used to get the display name for the Connection. Alternatively, it can also be a string that can contain parameters in curly braces. For security schemes with `"type": "apiKey"`, the `{apiKey}` paramater will be replaced with the API key provided by the user.

**Location**

* Security Scheme Object

**Parameters**

* `method` ... HTTP method (GET, POST, ...)
* `url` ... The endpoint to be called. If the URL is relative, it will be relative to the base URL (defined in the OpenAPI `servers` section).
* `headers` ... HTTP headers object.
* `query` ... HTTP query object (key-value pairs representing the query string parameters).
* `transform` ... JSONata expression to retrieve the value from the response payload that will be used as a display name for the Connection.

The values of the `url`, `headers`, and `query` parameters can contain parameters enclosed in curly braces. These parameters will be replaced by the values collected from the user during the authentication flow (the named parameters in the Security Scheme Object) or are implicitely provided (`accessToken` in case of OAuth).

**Examples**

**A common use in API keys-based authentication:**

```
"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-profile": {
                "method": "GET",
                "url": "/user?apiKey={apiKey}",
                "transform": "content.name"
            },
            ...
        }
    },
```

**A common use in API keys-based authentication with no /me endpoint:**

```
"components": {
    "securitySchemes": {
        "api_key_query": {
            "type": "apiKey",
            "name": "apiKey",
            "in": "query",
            "x-connector-connection-profile": "{apiKey}"
            ...
        }
    },
```

**A common use in Oauth-based authentication:**

```
"components": {
    "securitySchemes": {
      "OAuth": {
        "x-connector-connection-profile": {
          "method": "GET",
          "url": "/users/me",
          "headers": {
              "Authorization": "Bearer {accessToken}"
          },
          "transform": "email"
        },
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://zoom.us/oauth/authorize",
            "scopes": {},
            "tokenUrl": "https://zoom.us/oauth/token"
          }
        },
        "type": "oauth2"
      }
    }
}
```

#### x-connector-connection

**Description**

For API-key based authentication, the `x-connector-connection` extension is an alternative to the OpenAPI-native definition. The `x-connector-connection` is more compact and contains all the necessary parameters for the entire Connection definition (including checks and profile requests). Also, the standard OpenAPI `apiKey` security scheme does not have a mechanism to describe how exactly is the api key passed in the request. It only allows to define where it is passed (header, query). However, some APIs require the value of the api key parameter to be more than just the key. For example, the OpenAI API uses the following in the HTTP headers: `{ Authorization: "Bearer {apiKey}" }`. The Voys API even combines more api keys into one with: `{ Authorization: "token {username}:{apiKey}" }`. If the `x-connector-connection` extension is used, it overrides other authentication definitions from the `security` section.

**Location**

* Root.

**Parameters**

* `type` ... The type of the authentication scheme. Only `"apiKey"` is currently accepted.
* `in` ... The location of the authentication credentials (api key(s)) in all authentication requests. Can be `"header"`, `"query"` or `"cookie"`.
* `name` ... Name of the header, query parameter or cookie.
* `value` ... The template for the value of the authentication credential. It can use any property from the `schema` object enclosed in curly brackets. These will be replaced with real values at runtime.
* `schema` ... JSON schema describing the authentication parameters. The schema is used to generate the authentication screen for the user where the parameter values will be requested from the user in a HTML form. Only a flat object type of JSON-schema structure is supported at this point and only the `string` type is supported for the properties.
* `check` ... An HTTP request that will be called to validate whether the user authentication credentials (Connection) are valid. The same parameters as in the `x-connector-connection-check` extension are supported.
* `profile` ... An HTTP request that will be called to get the user profile (used mainly to get a display name for the Connection). The same parameters as in the `x-connector-connection-profile` extension are supported. Alternatively, the `profile` can also be a template string that can contain parameters defined in the `schema` in curly brackets.

**Examples**

**Two API keys (one username, one api key, custom value for Authorization header)**

```
"x-connector-connection": {
    "type": "apiKey",
    "in": "header",
    "name": "Authorization",
    "value": "token {username}:{apiKey}",
    "schema": {
        "type": "object",
        "properties": {
            "username": {
                "type": "string",
                "title": "Username"
            },
            "apiKey": {
                "type": "string",
                "title": "API Key"
            }
        }
    },
    "check": {
        "method": "GET",
        "url": "/callnotification/callnotification/",
        "headers": {
            "Authorization": "token {username}:{apiKey}"
        },
        "expect": {
            "status": 405
        }
    },
    "profile": "{username}"
},
```

**One API key (custom value for authorization header)**

```
"x-connector-connection": {
  "type": "apiKey",
  "in": "header",
  "name": "Authorization",
  "value": "Bearer {apiKey}",
  "schema": {
      "type": "object",
      "properties": {
          "apiKey": {
              "type": "string",
              "title": "API Key",
              "description": "Log into your OpenAI account and find your API key."
          }
      }
  },
  "check": {
      "method": "GET",
      "url": "/models",
      "headers": {
          "Authorization": "Bearer {apiKey}"
      }
  },
  "profile": "{apiKey}"
}
```

#### x-connector-label

**Description**

A custom label for the generated Appmixer component. By default, the label of the component is the `operationId` defined in the Operation Object.

**Location**

* Operation Object

**Value**

A string.

**Example**

```
...
operationId: 'postContacts',
x-connector-label: 'CreateContact'
...
```

#### x-connector-description

**Description**

A custom description for the generated Appmixer component. By default, the description of the component is of the form `<label>summary</label></br>description`.\`

**Location**

* Operation Object

**Value**

A string.

**Example**

```
...
x-connector-description: 'Create a new contact or update an existing one.'
...
```

#### x-connector-field-index

**Description**

In OpenAPI specification, `parameters` and `requestBody` fields are typically not ordered by their importance from the perspective of a user filling a form that contains those fields. Therefore, in many cases, required fields or important fields may end up at the bottom of such a form. This is obviously not a great user experience. The `x-connector-field-index` allows you to give fields an order that you'd prefer when the fields are rendered in a form for the user to configure (Appmixer Inspector panel).

**Location**

* Schema Object

**Value**

An integer.

**Example (parameters)**

```
...
"parameters": {
    "in": "query",
    "name": "accountId",
    "required": true,
    "schema": {
        "type": "string",
        "x-connector-field-index": 1
    }
...
```

**Example (requestBody)**

```
...
"requestBody": {
    "content": {
        "application/json": {
            "schema": {
                "type": "object",
                "properties": {
                    "firstName": {
                        "type": "string",
                        "x-connector-field-index": -1
                    }
                }
            }
        }
    }
}
...
```

#### x-connector-field-options

**Description**

Since not all options can be defined using the JSON schema, the `x-connector-field-options` makes it possible to add additional options to the generated Appmixer inspector field. Also note that the field options are processed after all other heuristics used to automatically convert JSON schemas to Appmixer inspector field took place. Therefore, if the generated inspector field is not what you expect, you can use the `x-connector-field-options` to override the generated setting.

**Location**

* Schema Object

**Value**

An object.

**Example**

```
...
"requestBody": {
    "content": {
        "application/json": {
            "schema": {
                "type": "object",
                "properties": {
                    "start_time": {
                        "type": "string",
                        "format": "date-time",
                        "x-connector-field-options": {
                            "format": "MM-ddTHH:mm:ss"
                        }
                    }
                }
            }
        }
    }
}
...
```

#### x-connector-transform

**Description**

**Location**

* Request Body Object

**Parameters**

* `language` ... the language of the expression. Currently only JavaScript is supported.
* `expression` ... the expression to transform the request body. The request body is available with the `requestBody` variable inside the expression. For example, the expression `requestBody = {}` empties the request body object completely, ignoring everything the user has set in the inspector.

**Example**

```
...
"requestBody": {
    "content": {
        "application/json": {
            "x-connector-transform": {
                "language": "javascript",
                "expression": "requestBody.start_time = requestBody.start_time.replace('Z', ''); requestBody.timezone = requestBody.timezone || 'UTC';",
            },
            "schema": {
                "type": "object",
                "properties": {
                    "start_time": {
                        "type": "string",
                        "format": "date-time"
                    }
                }
            }
        }
    }
}
...
```

#### x-connector-source

**Description**

Turn a property into a select box with options loaded from another source.

**Location**

* Schema Object (Any property in either `requestBody` or `parameters`).

**Parameters**

* `operationId` ... the ID of the operation within the OpenAPI spec that will be called to retrieve the data for the options of the select box. The resulting data will be read from the `out` output port.
* `transform` ... A [JMESPath](https://jmespath.org/) expression that must transform the resulting data into an array of objects with `value` and `label` fields. The `label` field is the visible name of the option to the user. The `value` field is the value that what will be used as the value for the field for which the `x-connector-source` extension is defined. \[Note that JMESPath is used instead of JSONata - prevalent in other extensions - since the Appmixer [tranform functions](https://docs.appmixer.com/appmixer/component-definition/manifest/properties#properties.source.data.transform) cannot be currently asynchronous.]
* `parameters` ... \[optional] A map of parameters that will be propagated to the source operation. An object with keys representing parameters of the source operation and values that are JSON pointers pointing to data of the operation that will propagate to the source operation. The root of the JSON pointer points to the operation under which the `x-connector-source` extension is defined.
* `requestBody` ... \[optional] A map of request body parameters that will be propagated to the source operation. An object with keys representing request body parameters (nested parameters are expressed using the `/` character) of the source operation and values that are JSON pointers pointing to data of the operation that will propagate to the source operation. The root of the JSON pointer points to the operation under which the `x-connector-source` extension is defined.

**Example (typical use)**

```
...
"requestBody": {
  "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "properties": {
          "board": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "x-connector-source": {
                  "operationId": "getServiceBoards",
                  "transform": "result[].{value: id, label: name}"
                }
              }
            }
          }
        }
      }
    }
  }
}
...
```

Consider an OpenAPI specification for Connectwise (`examples/openapi/connectwise/openapi.json`) for an endpoint that updates a service ticket (`putServiceTicketsById`). This endpoint takes `board.id` parameter in the request body to identify the service board the ticket should be placed in. If we left the OpenAPI spec without any modifications, the generated component would request the `board.id` from the user on an as-is basis, i.e. it would ask the user to provide the ID of the service board in a text input field. As you can imagine, this is not very helpful to the user since the user does not know that value. One way for the user to get around this would be to use another component such as `getServiceBoards` to search all the service boards and use the `id` output of the board found as an input of our `board.id`. However, in this case, it is much more user friendly to just show a list of service board names to the user right inside our `putServiceTicketsById` component and use the selected board's `id` automatically without the user having to deal with plain IDs.

**Example (parameters propagation)**

Sometimes, the source operation needs some parameters that are defined by the user in the design phase. To specify which parameters are propagated to the source operation, define the `parameters` and `requestBody` sections. In the example below, the `userId` is a parameter of the source operation and `{ form: { type: ... } }` is part of the request body of the source operation. Assume that both parameters are required and without which the source component cannot return any results (and fails for missconfiguration). We can simply provide a mapping of the parameters and request body data that will be propagated to the source component like this:

```
                        "x-connector-source": {
                            "operationId": "GetUserForms",
                            "transform": "result[].{value: id, label: title }",
                            "parameters": {
                                "userId": "/parameters/userId"
                            },
                            "requestBody": {
                                "form/type": "/requestBody/content/application~1json/schema/properties/formType"
                            }
                        }
```

#### x-connector-pagination

**Description**

Define a pagination method. This is especially useful for components that call endpoints that return an array of values (as opposed to a single object). In such cases, we may want to let the user configure how many items to return. At the same time, this limit on the number of items can be higher than the endpoint allows to return in one call (i.e. higher than the common `limit` paramater often used in the offset-limit type of pagination). If the `x-connector-pagination` is defined, the generated component will be smart enough to call the endpoint with varying parameters multiple times to retrieve the desired number of items.

**Location**

* Operation Object

**Parameters**

* `type` ... the type of the pagination used. One of `"page"`, `"cursor"`, `"link-header"` and `"once"`.
* `parameters` ... the parameters of the pagination specific to each type.

**`page` type pagination parameters**

* `offset` ... The name of the query parameter of the 3rd party endpoint that represents the (zero-based) offset of the first item returned in the collection.
* `limit` ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.
* `page` ... The number of items to return in one call of the endpoint. This number will be added to the `offset` parameter whith each subsequent call of the endpoint.
* `results` ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example `content.items`.
* `count` ... \[optional] A JSONata expression that is evaluated on the response data that returns the total number of items in the collection. This is a hint to the component to stop calling the endpoint when all results have been received. The most common value just points to the field from the response data that contains the total count of items. Example: `resultSet.count`.
* `more` ... \[optional] A JSONata expression that is evaluated on the response data that returns a boolean value that tells teh component to stop calling the endpoint in order to get more results. In other words, if `more` evaluates to false, the component knows it has collected all the results. The most common value points to the boolean field from the response data that contains informatoin on where there are more items. Example: `hasMore`.

```
"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "page",
            "parameters": {
                "offset": "offset",
                "limit": "limit",
                "page": 20,
                "results": "content",
                "count": "resultSet.count"
            }
        }
        ...
    }
}
```

**`cursor` type pagination parameters**

* `limit` ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.
* `page` ... The number of items to return in one call of the endpoint.
* `results` ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example `content.items`.
* `next` ... A JSONata expression that is evaluated on the response data that should return the value of the cursor, i.e. the next cursor. This is typically the ID of the next item starting from which we want retrieve the next batch of results.
* `cursor` ... The name of the query parameter of the 3rd party endpoint that represents the cursor.

```
"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "cursor",
            "parameters": {
                "limit": "limit",
                "page": 20,
                "results": "content",
                "cursor": "since",
                "next": "next_object"
            }
        }
        ...
    }
}
```

**`link-header` type pagination parameters**

This pagination expects the response to contain the `Link` HTTP header that contains at least one URL that, if requested, returns the next batch of items. This URL must have the `rel="next"` parameter set. For example:

```
Link: <https://api.github.com/repositories/1300192/issues?page=2>; rel="prev", <https://api.github.com/repositories/1300192/issues?page=4>; rel="next", <https://api.github.com/repositories/1300192/issues?page=515>; rel="last", <https://api.github.com/repositories/1300192/issues?page=1>; rel="first"
```

* `limit` ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.
* `page` ... The number of items to return in the first call of the endpoint. (Subsequent calls are assumed to have this parameter automatically added by the server returning the next URL in the `Link` HTTP header.)

```
"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "link-header",
            "parameters": {
                "limit": "limit"
            }
        }
        ...
    }
}
```

**`once` type pagination parameters**

This special type of pagination makes it easy to introduce paginated "features" (Limit inspector field, `result` output variable) on endpoints that do not support pagination but return an array of items. This is typically the case for endpoints that, even though return an array of items, do not return "much" of the items so no pagination is defined and necessary.

* `results` ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example `content.items`.

```
"/user/reports": {
    "get": {
        "summary": "Get User Reports",
        "operationId": "GetUserReports",
        "x-connector-pagination": {
            "type": "once",
            "parameters": {
                "results": "content"
            }
        }
        ...
    }
}
```

#### x-connector-webhook

**Description**

Define a webhook trigger. Two common types of webhooks are supported:

* A "subscription" type of webhook requires the 3rd party API to provide two endpoints for dealing with webhooks: subscribe and unsubscribe. The "subscribe" endpoint accepts a URL that will be called by the 3rd party to notify of new events occuring. The "unsubscribe" endpoint makes it possible to tell the 3rd party to remove the subscribed webhook from its registry and therefore stop receiving events on that URL.
* A "static" type of webhook allows to manually register a global webhook URL with the 3rd party that the 3rd party will notify of new events occuring. In this case, both the subscribing and unsubscribing of the webhook is done manually (usually as part of the OAuth app configuration).

**Location**

* Path Item Object of the `webhooks` section.

**Parameters**

**`type="subscription"` webhook parameters**

* `type` ... The type of the webhook. For the subscription type, use `"subscription"`.
* `subscribe` ... An object defining the HTTP request to be sent to subscribe a webhook with the 3rd party to receive events to.
* `subscribe.url` ... The URL of the request.
* `subscribe.method` ... The HTTP method of the request.
* `subscribe.headers` ... The HTTP headers of the request.
* `subscribe.body` ... The data payload of the request.
* `unsubscribe` ... An object defining the HTTP request to be sent to unsubscribe a webhook from the 3rd party in order to stop receiving events to the previously subscribed URL.
* `unsubscribe.url` ... The URL of the request. A special template parameters can be used to
* `unsubscribe.method` ... The HTTP method of the request.
* `unsubscribe.headers` ... The HTTP headers of the request.
* `unsubscribe.body` ... The data payload of the request.
* `outputCondition` ... \[optional] A JSONata expression that must evaluate to true in order for the trigger to output data in reaction to an incoming event that arrives at the webhook URL.
* `outputTransform` ... \[optional] A JSONata expression that can be used to transform the output of the trigger. The expression evaluates on the incoming request payload.

Special placeholders can be used in the `url`, `headers` and `body` fields of the `subscribe` and `unsubscribe` parameters. These will be replaced at runtime and include:

* `{$baseUrl}` ... This will be replaced by the base URL of the 3rd party service (see the `servers` section of the OpenAPI specification). Usable both in `subscribe` and `unsubscribe` objects.
* `{$webhookUrl}` ... The Webhook URL of the trigger component. Usable both in `subscribe` and `unsubscribe` objects.
* `{$request.body}` ... The entire request body. See [OpenAPI runtime expressions](https://swagger.io/docs/specification/links/) for more details. Usable mainly in the `output` parameter.
* `{$response.body#/foo/bar}` ... A portion of the response body specified by a JSON Pointer. See [OpenAPI runtime expressions](https://swagger.io/docs/specification/links/) for more details. Usable mainly in the `unsubscribe` object to point to fields from the payload returned in the response to the subscribe endpoint call. Typically, the response contains the ID of the webhook which we want to use in the request to unsubscribe from the webhook later on.
* `{$response.transform#JSONATA_EXPRESSION}` ... The response transformed using the JSONata expression language. This gives you the most expressive power - if needed. Sometimes, the `{$response.body#/foo/bar}` that uses the JSON Pointer does not have enough expressive power to extract the ID of the webhook to unsubscribe. In these cases, use this parameter together with a JSONata expression to query the data you need to unsubscribe the webhook.
* `{$response.header.header_name}` ... The value of the specified response header. See [OpenAPI runtime expressions](https://swagger.io/docs/specification/links/) for more details.
* `{$parameters.parameter_name}` ... The value of the specified parameter from the `parameters` section of the OpenAPI operation. `parameters` make it possible to parametrize the webhook trigger (request information from the user). Use the `{$parameters.parameter_name}` placeholder to reference the parameter values in both the `subscribe` and `unsubscribe` objects.
* `{$connection.profile#/foo/bar}` ... A portion of the connection profile object (see `x-connector-connection-profile`). This is especially useful to get values for data such as user ID, account ID or other, that are returned from the `/me`-type of endpoint. Use JSON Pointer after the `#` character to get to any nested values.

**Example: using of parameters, output transformation**

```
"webhooks": {
    "NewContact": {
        "post": {
            "x-connector-webhook": {
                 "type": "subscription",
                 "subscribe": {
                    "url": "{$baseUrl}/system/callbacks",
                    "method": "POST",
                    "body": {
                        "url": "{$webhookUrl}",
                        "objectId": "{$parameters.objectId}",
                        "type": "Contact",
                        "level": "{$parameters.level}"
                    }
                },
                "unsubscribe": {
                    "url": "{$baseUrl}/system/callbacks/{$response.body#/id}",
                    "method": "DELETE",
                    "body": null
                },
                "outputCondition": "$boolean(Action='added')",
                "outputTransform": "$eval(Entity)"
            },
            "operationId": "NewContact",
            "summary": "Triggers when a new company contact was created.",
            "parameters": [{
                "name": "level",
                "in": "body",
                "description": "When set to owner, all ConnectWise PSA contacts are returned. When set to type, all contacts of the specified type are returned. When set to territory, all contacts of the specified territory are returned. When set to company, all contacts of the specified company are returned. When set to contact, the specified contact is returned.",
                "required": false,
                "schema": {
                    "type": "string", "enum": ["Owner", "Type", "Territory", "Company", "Contact"],
                    "default": "Owner"
                }
            }, {
                "name": "objectId",
                "in": "body",
                "description": "The ObjectId should be the Id of whatever record you are subscribing to. This should be set to 1 when using a level of Owner.",
                "required": false,
                "schema": { "type": "integer", "default": 1 }
            }],
            "requestBody": {
                "description": "New Contact Created",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Contact"
                        }
                    }
                }
            },
            "responses": {
                "200": {
                    "application/json": {}
                }
            }
        }
    }
}
```

**Example: a more complex unsubscribe URL with transforms**

```
"webhooks": {
    "NewFormSubmission": {
        "post": {
            "operationId": "NewFormSubmission",
            "summary": "Triggers when a form is submitted.",
            "x-connector-webhook": {
                "type": "subscription",
                "subscribe": {
                    "url": "{$baseUrl}/form/{$parameters.formId}/webhooks",
                    "method": "POST",
                    "headers": { "Content-Type": "application/x-www-form-urlencoded" },
                    "body": { "webhookURL": "{$webhookUrl}" }
                },
                "unsubscribe": {
                    "url": "{$baseUrl}/form/{$parameters.formId}/webhooks/{$response.transform#$keys(data.content) ~> $filter(function ($key) { $lookup(data.content, $key) = \"{$webhookUrl}\"})}",
                    "method": "DELETE",
                    "body": null
                },
                "outputCondition": "$exists(submissionID)"
            },
            "parameters": [{
                "name": "formId",
                "in": "body",
                "description": "Form ID",
                "required": true,
                "schema": {
                    "type": "string",
                    "x-connector-source": {
                        "operationId": "GetUserForms",
                        "transform": "result[].{value: id, label: title }"
                    }
                }
            }],
            "requestBody": {
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/WebhookSubmission"
                        }
                    }
                }
            },
            "responses": {
                "200": {
                    "content": {
                        "application/json": {
                        }
                    }
                }
            }
        }
    }
}
```

**`type="static"` webhook parameters**

* `type` ... The type of the webhook. For the static type, use `"static"`.
* `path` ... The endpoint of the static webhook. The entire URL will be `{APPMIXER_API_URL}/plugins/appmixer/{SERVICE}{PATH}`. For example: `https://api.appmixer.com/plugins/appmixer/zoom/events`.
* `pattern` ... A JSONata expression that is evaluated on the incoming webhook request (with `payload`, `headers`, `method` and `query` fields). If the `topic` parameter matches the evaluated `pattern`, the component triggers and outputs the incoming request payload (considering `outputCondition` is met).
* `topic` ... The value of the evaluated `pattern` for which the component is interested in receiving events.
* `outputCondition` ... \[optional] A JSONata expression that must evaluate to true in order for the trigger to output data in reaction to an incoming event that arrives at the webhook URL.
* `outputTransform` ... \[optional] A JSONata expression that can be used to transform the output of the trigger. The expression evaluates on the incoming request payload.
* `crc` ... The definition of the Challenge-Response check. Some APIs use the challenge-response check to confirm the ownership and the security of the webhook notification endpoint URL. In this scenario, the API sends a POST request to the webhook URL with a challenge (a token) and the webhook endpoint must return the challenge hashed with a secret token known to both sides.
* `crc.condition` ... A JSONata expression evaluated on the incoming request (with `payload`, `headers`, `method` and `query` fields) that must evaluate to true for the incoming request to be considered as the challenge request (i.e. to ignore all non-CRC requests).
* `crc.alg` ... The hash function used to calculate HMAC. Typically `"sha256"`.
* `crc.key` ... The name of the configuration value of the connector that contains a secret token known to both sides, that will be passed to the HMAC to calculate the final token for the response. This points to the configuration key that can be set in the Appmixer Backoffice -> Configuration for the service `appmixer:SERVICE`.
* `crc.challenge` ... A JSONata expression evaluated on the request returning the challenge token (that will be used together with the `key` to calculate the HMAC).
* `crc.digest` ... The encoding of the HMAC. Supported values are `"hex"`, `"base64"` and `"utf8"`.
* `crc.response` ... A JSONata expression evaluated on an object of the form `{ responseToken, challenge }` containing both the original `challenge` and the final calculated HMAC (`responseToken`). The expression should return the payload to be send as a response to the CRC request.

```
{
  "webhooks": {
    "meeting.deleted": {
      "post": {
        "operationId": "meetingDeleted",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payload|object": {
                    "type": "object",
                    "description": "Information about the meeting.",
                    "properties": {
                      "uuid": {
                        "type": "string",
                        "description": "The meeting's universally unique identifier (UUID)."
                      },
                      "topic": {
                        "type": "string",
                        "description": "The meeting's topic."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-connector-webhook": {
          "type": "static",
          "path": "/events",
          "pattern": "payload.event & ':' & payload.payload.account_id",
          "topic": "meeting.deleted:{$connection.profile.account_id}",
          "crc": {
            "condition": "payload.event = \"endpoint.url_validation\"",
            "alg": "sha256",
            "key": "webhookSecretToken",
            "challenge": "payload.payload.plainToken",
            "response": "{ \"encryptedToken\": responseToken, \"plainToken\": challenge }",
            "digest": "hex"
          }
        }
      }
    }
  }
}
```

**`cursor` type pagination parameters**

* `limit` ... The name of the query parameter of the 3rd party endpoint that represents the maximum number of entries to return.
* `page` ... The number of items to return in one call of the endpoint.
* `results` ... A path to the data returned from the endpoint that represents the array of items. This path can point to nested properties by using the dot character to separate levels. For example `content.items`.
* `next` ... A JSONata expression that is evaluated on the response data that should return the value of the cursor, i.e. the next cursor. This is typically the ID of the next item starting from which we want retrieve the next batch of results.
* `cursor` ... The name of the query parameter of the 3rd party endpoint that represents the cursor.

```
"/user/forms": {
    "get": {
        "summary": "Get User Forms",
        "operationId": "GetUserForms",
        "x-connector-pagination": {
            "type": "cursor",
            "parameters": {
                "limit": "limit",
                "page": 20,
                "results": "content",
                "cursor": "since",
                "next": "next_object"
            }
        }
        ...
    }
}
```

#### x-connector-rel-link-base-url

**Description**

Define a URL base path for any link with a relative URL in the connector. Applies only to relative URL links in markdown formatted descriptions. When provided, it will convert `[link text](/documentation#endpoints-price-range)` to `<a href="https://www.boredapi.com/documentation#endpoints-price-range">link text</a>`.

**Location**

* Info Object

**Value**

A string.

**Example**

```
"info": {
  ...
  "x-connector-rel-link-base-url": "https://www.boredapi.com"
  ...
}
```

### Examples

#### BoredAPI

* A minimalistic OpenAPI spec for the single-endpoint <https://www.boredapi.com/> API.
* No JSON patch, no OpenAPI extensions are used.
* OpenAPI Spec

```
appmixer init openapi ./boredapi/openapi.json ./connector/
```

#### Jotform

* A custom written single OpenAPI spec with extensions right inside the OpenAPI document.
* Pagination, connection check/profile, source, custom API-key based authentication, webhooks.
* OpenAPI Spec

Used extensions:

* x-connector-icon
* x-connector-service
* x-connector-module
* x-connector-webhook
* x-connector-source
* x-connector-pagination
* x-connector-connection-check
* x-connector-connection-profile

#### Zoom

* OpenAPI with a separate JSON Patch file with changes and fixes, separate OpenAPI spec for webhook definitions externally referenced from the OpenAPI spec.
* File upload (multipart/form-data), OAuth 2, pagination (cursor), inspector field index, request body JavaScript transformation.
* OpenAPI Spec
* OpenAPI Webhooks Spec
* JSON Patch

```
appmixer init openapi ./zoom/ZoomMeetingAPISpec.json --patch ./zoom/openapi.json-patch --artifacts ./connector
```

Used extensions:

* x-connector-icon
* x-connector-service
* x-connector-module
* x-connector-webhook
* x-connector-pagination
* x-connector-connection-check
* x-connector-connection-profile
* x-connector-field-index
* x-connector-transform

#### OpenAI

* OpenAPI in YAML format with a separate JSON Patch file with changes and fixes.
* File upload (multipart/form-data), custom API key-based auth, source, pagination (once), inspector field index.
* OpenAPI Spec
* JSON Patch

```
appmixer init openapi --artifacts --patch ./openai/openapi.json-patch ./openai/openapi.yaml ./connector
```

Used extensions:

* x-connector-icon
* x-connector-service
* x-connector-module
* x-connector-pagination
* x-connector-connection
* x-connector-field-index
* x-connector-source

### Supported OpenAPI versions

All versions starting from version 2 (Swagger) is supported, i.e. `2.0`, `3.0.x` and `3.1`. Note that the `2.0` version is automatically detected and a conversion to version `3.0.x` is automatically performed using the [swagger2openapi](https://github.com/Mermade/oas-kit/blob/main/packages/swagger2openapi/README.md) npm package.

Also note that the tool accepts both YAML and JSON files as inputs for the OpenAPI specification.

### Notes & Limitations

* Fields in `parameters` and `requestBody` are mixed together to form a flat list of configuration fields of the connector. Conflicting names are not accepted at this point.
* Only the first subschema of the `oneOf` JSON schema composition keyword is used.
* `allOf` subschemas are merged into one using the (json-schema-merge-allof)\[<https://www.npmjs.com/package/json-schema-merge-allof>] library.
* Only the first subschema of the `anyOf` JSON schema composition keyword is use.d
* Only the first subschema of the `oneOf` JSON schema composition keyword is used.
* The output JavaScript code is formatted using (ESLint)\[<https://eslint.org/>].

<br>


# Installation Docker Compose

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

## Prerequisites

* [Docker](https://www.docker.com)
* [Docker Compose](https://docs.docker.com/compose/)
* [NodeJS](https://nodejs.org) v24.6.0 (only for [Appmixer CLI](/appmixer-cli/appmixer-cli#installation))

## Installation

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

#### Install and Start Appmixer

```bash
docker-compose --project-name appmixer up
```

#### Stop Appmixer

```bash
docker-compose --project-name appmixer stop
```

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

#### Stop and Clean Up

Stop Appmixer and remove all containers and images:

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

## Using Webhook Components

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

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

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

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

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

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

with the URL from ngrok:

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

Now restart Appmixer:

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

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

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

But this command will set the GRIDD\_URL to `https://568284c4.ngrok.io` as well.

{% hint style="info" %}
The information about automatically setting the GRIDD\_URL is valid only when our `docker-componse`.yml file is used. When you run Appmixer without it, the GRIDD\_URL has to be set. This variable affects the [OAuth redirect URL](/building-connectors/authentication#oauth-2-redirect-uri).
{% endhint %}

## Creating an Admin User

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

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

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

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

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

![](/files/-LvpN_uDKOBBM4ltZpRB)

## Enabling Users to Publish Custom Components

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

{% hint style="info" %}
Note that the Appmixer Trial package automatically sets the "appmixer" vendor on ALL newly created users, so any user of the Trial package can publish custom components with no extra action required.
{% endhint %}

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

![](/files/-LvpPTaRpTyWmmWY6Pbm)

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


# Installation Helm Chart

A comprehensive Helm chart to deploy [Appmixer](https://www.appmixer.com/) platform and all its dependencies on Kubernetes

***

### 📋 Prerequisites

* **Kubernetes** 1.29+
* **Helm** 3.19+
* **kubectl** configured to access your cluster
* **AWS ECR credentials** (provided by Appmixer)
* **Domain name** for exposing services

#### What Gets Deployed

This chart automatically deploys:

* **Appmixer Services**: engine-api, engine-worker, frontend, backoffice, quota
* **MongoDB** ReplicaSet (3 replicas + arbiter)
* **Redis Sentinel** (3 replicas)
* **RabbitMQ** cluster (3 replicas)
* **Elasticsearch** + **Kibana** (via ECK operator)
* **Logstash** for log processing

***

### 🚀 Installation Guide

#### Step 1: Setup ECR Authentication (Automated)

Appmixer images are hosted in AWS ECR. ECR tokens expire every 12 hours, so we use a CronJob to automatically refresh them.

**1.1. Create ECR Token Refresh CronJob**

Save the following as `ecr-token-refresh.yaml` and **replace placeholders** with credentials provided by Appmixer:

```yaml
apiVersion: v1
kind: Namespace
metadata:
  name: appmixer
---
apiVersion: v1
kind: Secret
metadata:
  name: ecr-credentials
  namespace: appmixer
type: Opaque
stringData:
  AWS_ACCESS_KEY_ID: "YOUR_ACCESS_KEY_ID"
  AWS_SECRET_ACCESS_KEY: "YOUR_SECRET_ACCESS_KEY"
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ecr-token-refresher
  namespace: appmixer
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: ecr-token-refresher
  namespace: appmixer
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "create", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ecr-token-refresher
  namespace: appmixer
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: ecr-token-refresher
subjects:
- kind: ServiceAccount
  name: ecr-token-refresher
  namespace: appmixer
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: ecr-token-refresh
  namespace: appmixer
spec:
  schedule: "0 */10 * * *"  # Every 10 hours
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: ecr-token-refresher
          restartPolicy: OnFailure
          containers:
          - name: refresh-token
            image: amazon/aws-cli:latest
            envFrom:
            - secretRef:
                name: ecr-credentials
            command:
            - /bin/bash
            - -c
            - |
              set -e
              echo "Installing kubectl..."
              curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
              chmod +x kubectl
              mv kubectl /usr/local/bin/

              echo "Getting ECR login token..."
              TOKEN=$(aws ecr get-login-password --region eu-central-1)

              echo "Creating/updating docker-registry secret..."
              kubectl create secret docker-registry ecr-registry-secret \
                --docker-server=874193467787.dkr.ecr.eu-central-1.amazonaws.com \
                --docker-username=AWS \
                --docker-password="$TOKEN" \
                --namespace=appmixer \
                --dry-run=client -o yaml | kubectl apply -f -

              echo "ECR token refresh completed successfully"
```

**1.2. Deploy the CronJob**

```bash
# Apply the configuration
kubectl apply -f ecr-token-refresh.yaml

# Verify CronJob was created
kubectl get cronjob -n appmixer

# Manually trigger the first run to create the initial secret
kubectl create job --from=cronjob/ecr-token-refresh ecr-token-refresh-manual -n appmixer

# Wait ~30 seconds, then verify the secret was created
kubectl get secret ecr-registry-secret -n appmixer
```

> **Note**: The CronJob runs every 10 hours to ensure the token is always valid (tokens expire after 12 hours).

***

#### Step 2: Install Elasticsearch Operator (ECK)

Appmixer uses Elasticsearch (managed by ECK operator) for logging and monitoring.

> **Note**: Check for the latest ECK version at [Elastic Cloud on Kubernetes Downloads](https://www.elastic.co/downloads/elastic-cloud-kubernetes)

```bash
# Install ECK CRDs
kubectl create -f https://download.elastic.co/downloads/eck/3.3.0/crds.yaml

# Install ECK operator
kubectl apply -f https://download.elastic.co/downloads/eck/3.3.0/operator.yaml

# Verify the operator is running
kubectl get pods -n elastic-system
```

***

#### Step 3: Pull Appmixer Helm Chart

Log in to AWS ECR Helm registry using credentials provided by Appmixer:

```bash
# Login to ECR Helm registry
aws ecr get-login-password --region eu-central-1 | \
  helm registry login --username AWS --password-stdin \
  874193467787.dkr.ecr.eu-central-1.amazonaws.com
```

Pull the Helm chart:

```bash
# Pull latest version
helm pull oci://874193467787.dkr.ecr.eu-central-1.amazonaws.com/appmixer-helm

# OR pull specific version
helm pull oci://874193467787.dkr.ecr.eu-central-1.amazonaws.com/appmixer-helm \
  --version <CHART_VERSION>

# Extract the chart
tar -xzf appmixer-helm-*.tgz
cd appmixer-helm
```

***

#### Step 4: Configure values.yaml

Edit `values.yaml` to match your environment:

**Important settings to review:**

* `ingress.domain` - Your domain name
* `ingress.className` - `nginx`, `alb`, or your ingress controller
* `expose.method` - Choose: `ingress`, `aws-alb`, or `routes` (OpenShift)

***

#### Step 5: Install Appmixer

```bash
# Create namespace for Appmixer (if not using the one from Step 1)
export NAMESPACE=appmixer

# Install the chart
helm upgrade --install appmixer . \
  --namespace $NAMESPACE \
  --create-namespace \
  --timeout 15m

# Watch the deployment
kubectl get pods -n $NAMESPACE -w
```

***

### 🌐 Accessing Appmixer

After installation completes (typically 5-10 minutes), retrieve your access URLs:

#### For Kubernetes Ingress

```bash
kubectl get ingress -n $NAMESPACE
```

#### For OpenShift Routes

```bash
kubectl get routes -n $NAMESPACE
```

#### For AWS ALB

```bash
kubectl get ingress -n $NAMESPACE -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}'
```

#### Default Admin Credentials

If you left `userInit.password` empty, retrieve the auto-generated password:

```bash
kubectl get secret appmixer-secrets -n $NAMESPACE \
  -o jsonpath='{.data.ADMIN_PASSWORD}' | base64 -d
```

Login at: `https://my.your-domain.com` with the email from `userInit.email`

***

### 🔄 Upgrading Appmixer

#### Upgrade to New Version

1. Update `Chart.yaml`:

```yaml
appVersion: "6.3.3"  # Change to desired version
```

2. Apply the upgrade:

```bash
helm upgrade --install appmixer . -n $NAMESPACE
```

#### Update Configuration Only

```bash
# Edit values.yaml, then:
helm upgrade appmixer . -n $NAMESPACE
```


# Installation AWS ECS

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

## Prerequisities

* [AWS account](https://aws.amazon.com/)
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
* [Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli)
* [Cofigured AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)
* Account in Appmixer Docker registry \[registry.appmixer.com]. This account has been given to you during your sign-up process to the Appmixer Self-Managed package.

## Installation Types

There are two types of installations:

* Default installation
* Custom installation

### Default Installation

This type of installation assumes that you don't have any preferences regarding AWS Networking (VPC) and want leave all attributes in default. This kind of installation will create a new VPC and all required network parts (Internet Gateway, NAT Gateway, subnets, route tables, etc.). Then it will provision all the four required AWS managed services:

* Amazon DocumentDB (MongoDB)
* ElastiCache (Redis)
* Opensearch (Elasticsearch)
* Amazon MQ (RabbitMQ)

Next, it will provision AWS ECS (Elastic Container Service) with all Appmixer applications (Engine, Frontend, Backoffice, Quota and Logstash).

### Custom Installation

By configuring **external**\_\* values, you have a choice where to install the Appmixer stack and choose external services like:

* **external\_vpc**: your specific VPC
* **external\_redis**: external Redis (running either in AWS or somewhere else)
* **external\_rabbitmq**: external RabbitMQ (running either in AWS or somewhere else)
* **external\_elasticsearch**: external Elasticsearch (running either in AWS or somewhere else)
* **external\_documentdb**: external MongoDB (running either in AWS or somewhere else)

## Preparation

First, clone the [Appmixer Module AWS](https://github.com/clientIO/appmixer-module-aws) git project with the Terraform code:

```bash
$ git clone https://github.com/clientIO/appmixer-module-aws.git
$ cd appmixer-module-aws/
```

This repository contains examples for both dev and production use cases. Also, both of these examples include autoscaling configuration. Let's start with **dev** example:

```bash
$ cd examples/development
```

This example defines a new VPC and it's CIDR block. You will need to configure:

* **zone\_id** (line #44) -> if set AWS Route53 will be used and you should also update **aws\_route53\_zone** (line #33) and set the domain name to be used.
* **ecs\_autoscaling\_config** (line #52) -> Docker registry credentials, base64 encoded string.
* **input\_init\_user** (line #54) -> here you can configure credentials for the initial user (Appmixer admin).

## Installation

You should now be ready to provision the Appmixer stack.

<pre class="language-bash"><code class="lang-bash"><strong>$ terraform init
</strong>$ terraform plan
</code></pre>

In the ouptput of the above commands, you can see the exact resources that will be provisioned. If the output looks right we can proceed:

```bash
$ terraform apply
```

Again, you'll have a chance to check what exactly will be provisioned. If it looks correct, you can approve. After the Terraform run, you'll get the so called Terraform output, where you can find a lot of useful information like Loadbalancer name, service URLs, managed services, etc.


# Monitoring and Observability

### Overview

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

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

* MongoDB - Primary data store
* Redis - Caching
* RabbitMQ - Message queue for asynchronous processing
* Elasticsearch - Logs storage
* Logstash - Log processing

This document is organized into two main sections:

* Application Monitoring - Appmixer-specific metrics and health indicators
* Infrastructure Monitoring - Guidelines for monitoring the underlying services

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

### Application Monitoring

#### Appmixer Application Metrics

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

#### Application Health Endpoints

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

**Root Endpoint: GET /**

The root endpoint provides a basic liveness check and returns basic API information. This endpoint does not require authentication and can be used for:

* Kubernetes/OpenShift liveness probes - To detect if the pod is responsive
* Load balancer health checks - To verify the service is accepting connections
* Basic availability monitoring - To ensure the HTTP server is running

**Response Format:**

```json
{
  "name": "appmixer",
  "version": "6.2.0",
  "url": "http://api.appmixer.com",
  "studioUrl": "https://studio.appmixer.com",
  "integrationsUrl": "https://studio.appmixer.com/integrations"
}
```

**Response Fields:**

* `name` - Application name
* `version` - Appmixer version
* `url` - API endpoint URL
* `studioUrl` - Studio UI URL
* `integrationsUrl` - Integrations marketplace URL

**Monitoring Recommendations:**

* Use this endpoint for basic availability monitoring
* Expected response time: < 100ms
* Expected HTTP status: 200 OK
* Alert if response time > 500ms or status != 200
* Configure as liveness probe in OpenShift/Kubernetes

**System Health Endpoint: GET /system/health**

The system health endpoint provides detailed metrics about the Appmixer application's internal state and performance. This endpoint is designed for deep health monitoring and diagnostics.

**Authentication:**

* Requires API key authentication OR
* JWT authentication with admin scope

**Response Format:**

```json
{
  "inputQueue": {
    "messageCount": 42
  },
  "eventsListeners": {
    "total": 150,
    "byType": {
      "webhook": 80,
      "scheduled": 70
    }
  },
  "events": 1523,
  "listeners": 245,
  "actions": 890,
  "slowQueue": {
    "count": 5,
    "top": [
      {
        "flowId": "flow-123",
        "count": 3
      }
    ]
  },
  "systemWebhooks": {
    "registered": 12,
    "active": 10
  }
}
```

**Response Fields:**

* `inputQueue.messageCount` - Number of messages waiting in the main input queue for processing
  * This is the most critical metric for monitoring system load
  * Normal range: 0-1000 messages
  * Warning threshold: > 2000 messages
  * Critical threshold: > 10000 messages
  * High queue length indicates processing bottleneck or insufficient resources
* `eventsListeners` - Statistics about registered event listeners (webhooks, triggers)
  * `total` - Total number of active event listeners
  * `byType` - Breakdown by listener type
  * Helps monitor integration activity
* `events` - Total count of events in the system
  * Represents pending events waiting for listeners to process
  * High count may indicate listener processing issues
* `listeners` - Count of flow listeners
  * Number of components waiting for incoming data
* `slowQueue.count` - Number of flows currently in the slow queue
  * Flows experiencing repeated failures or slow performance
  * Should be monitored for troubleshooting
* `slowQueue.top` - Top 100 flows by slow queue occurrence
  * Identifies problematic flows requiring attention
  * Each entry includes `flowId` and `count`
* `systemWebhooks` - System webhook statistics (only available on worker nodes)
  * `registered` - Number of system webhooks configured
  * `active` - Number of currently active webhooks

**Monitoring Recommendations:**

Critical Metrics to Monitor:

* `inputQueue.messageCount` - Alert if > 2000 (warning) or > 10000 (critical)
* HTTP status - Alert if not 200 OK

Warning Indicators:

* `slowQueue.count` increasing over time
* `events` count growing continuously
* Response time degradation

Alert Examples:

* Critical: Input queue > 1000 messages for > 5 minutes
* Warning: Input queue > 500 messages for > 10 minutes
* Warning: Slow queue count > 10 flows
* Info: System webhooks registered != active

Dashboard Visualization:

* Line chart: `inputQueue.messageCount` over time
* Gauge: Current input queue size vs thresholds
* Table: Top slow queue flows
* Counter: Total events, listeners, actions

**Example cURL Request:**

```bash
# Using API key authentication
curl -H "X-API-Key: your-api-key" https://api.appmixer.com/system/health

# Using JWT token with admin scope
curl -H "Authorization: Bearer your-jwt-token" https://api.appmixer.com/system/health
```

**Integration with Monitoring Tools:**

Prometheus scraper configuration example:

```yaml
scrape_configs:
  - job_name: 'appmixer-health'
    metrics_path: '/system/health'
    scheme: https
    static_configs:
      - targets: ['api.appmixer.com']
    bearer_token: 'your-api-key'
    scrape_interval: 60s
```

#### Flow Execution Metrics

*This section will be expanded in a future update to include metrics for workflow execution times, success/failure rates, and active flow monitoring. For now, we recommend monitoring the `inputQueue.messageCount` and `slowQueue` metrics from the `/system/health` endpoint as primary indicators of flow execution health.*

#### Component Performance

*Detailed component-level performance monitoring guidance is planned for a future documentation update. In the meantime, standard Node.js application monitoring practices apply, and errors from individual connectors will appear in your Elasticsearch logs.*

### Infrastructure Monitoring

#### General Principles

For each infrastructure component, we recommend monitoring:

* **Availability** - Is the service up and responding?
* **Performance** - Response times, throughput, and latency
* **Resource Utilization** - CPU, memory, disk, and network usage
* **Error Rates** - Connection errors, timeouts, and failures
* **Capacity Planning** - Trends for storage, connections, and load

#### MongoDB Monitoring

MongoDB is the primary data store for Appmixer. Monitor the following metrics:

**Key Metrics**

**Database Performance**

* Query execution time (slow queries)
* Operations per second (reads/writes)
* Document scan rates
* Index usage and efficiency

**Replication (if using replica sets)**

* Replication lag
* Oplog window
* Member health status
* Election events

**Resource Usage**

* Memory utilization (resident and virtual)
* Disk I/O (read/write operations)
* Disk space usage and growth rate
* Network throughput

**Recommended Tools**

* MongoDB Atlas (for managed MongoDB)
* MongoDB Cloud Manager / Ops Manager
* Prometheus with MongoDB exporter
* Datadog, New Relic, or similar APM tools

**Alert Thresholds (Examples)**

* Replication lag > 10 seconds
* Disk usage > 80%
* Connection pool exhaustion > 90%
* Slow queries > 1000ms

#### Redis Monitoring

Redis is used for caching. Monitor the following:

**Key Metrics**

**Availability**

* Uptime
* Master-slave sync status
* Connection success rate

**Recommended Tools**

* Redis INFO command
* Prometheus with Redis exporter
* RedisInsight
* Cloud-native monitoring (if using managed Redis)

#### RabbitMQ Monitoring

RabbitMQ handles asynchronous message processing for Appmixer workflows and tasks.

**Key Metrics**

**Queue Health**

* Queue length (messages ready)
* Messages unacknowledged
* Message rate (publish/deliver/ack)
* Queue growth rate

**Connection and Channels**

* Failed connection attempts

**Node Health**

* Memory usage (high/low watermarks)
* Disk space (free/used)

**Cluster Health (if clustered)**

* Node availability
* Network partition events
* Mirror queue synchronization

**Recommended Tools**

* RabbitMQ Management Plugin
* Prometheus with RabbitMQ exporter
* Datadog, New Relic, or similar APM tools

**Alert Thresholds (Examples)**

* Queue length growing beyond normal capacity
* Memory usage > 80% of high watermark
* No consumers on critical queues
* Disk space < 20% free

#### Elasticsearch Monitoring

Elasticsearch provides search capabilities and stores operational data.

**Key Metrics**

**Cluster Health**

* Cluster status (green/yellow/red)
* Number of nodes

**Resource Usage**

* JVM heap usage
* JVM garbage collection time
* CPU usage per node
* Disk I/O per node

**Storage**

* Total disk space used
* Index size growth rate

**Recommended Tools**

* Kibana Monitoring
* Elasticsearch Monitoring API
* Prometheus with Elasticsearch exporter
* Cloud-native monitoring (if using managed Elasticsearch)

**Alert Thresholds (Examples)**

* Cluster status = red or yellow for > 5 minutes
* JVM heap usage > 85%
* Disk usage > 85%

#### Logstash Monitoring

Logstash processes and transforms log data before sending it to Elasticsearch.

**Key Metrics**

**Pipeline Performance**

* Events received/filtered/sent

**Recommended Tools**

* Logstash Monitoring API
* Kibana Monitoring
* Prometheus with Logstash exporter

**Alert Thresholds (Examples)**

* JVM heap usage > 85%
* Pipeline events duration increasing
* Dead letter queue growing
* Plugin errors increasing

#### OpenShift Platform Monitoring

Since Appmixer runs on OpenShift, leverage OpenShift's built-in monitoring capabilities:

**Key Aspects**

**Pod Health**

* Pod status (Running/Failed/Pending)
* Restart counts
* Container resource usage vs limits

**Resource Quotas**

* Namespace CPU/memory usage
* Storage usage
* Pod count vs limits

**Network**

* Service availability
* Ingress/route response times
* Network policy effectiveness

**Persistent Volumes**

* Volume usage
* Volume performance metrics
* Volume mount issues

**Tools**

* OpenShift Monitoring (Prometheus-based)
* OpenShift Web Console
* oc CLI monitoring commands

### Recommended Monitoring Stack

#### Option 1: Prometheus + Grafana (Open Source)

* Prometheus for metrics collection
* Grafana for visualization and dashboards
* Alertmanager for alert routing
* Exporters for each infrastructure component
* Custom exporters for Appmixer application metrics

#### Option 2: Commercial APM Solutions

* New Relic
* Datadog
* Dynatrace
* AppDynamics

#### Option 3: Hybrid Approach

* Use OpenShift's built-in Prometheus for infrastructure
* Add custom Grafana dashboards
* Integrate with existing enterprise monitoring tools

### Alerting Strategy

#### Alert Severity Levels

**Critical** - Immediate action required, service degradation or outage

* Production outage
* Data loss risk
* Security breach

**Warning** - Attention needed, potential issues developing

* Resource usage approaching limits
* Performance degradation
* Increased error rates

**Info** - Informational, no immediate action needed

* Deployment notifications
* Configuration changes
* Capacity planning indicators

#### Alert Best Practices

* Define clear runbooks for each alert
* Avoid alert fatigue by tuning thresholds
* Use alert aggregation to reduce noise
* Implement escalation policies
* Test alerting channels regularly

### Logging Strategy

#### Log Aggregation

* Set appropriate log retention policies based on compliance requirements and storage capacity

#### Log Levels

Use appropriate log levels:

* **ERROR** - Application errors requiring attention
* **WARN** - Warning conditions
* **INFO** - Informational messages
* **DEBUG** - Detailed diagnostic information (non-production)

#### Key Logs to Monitor

* Application startup/shutdown events
* Authentication and authorization failures
* API request/response logs (with sampling)
* Integration connector errors
* Database connection issues
* Message queue processing errors

### Performance Tuning

Based on monitoring data, consider these tuning areas:

#### Node.js Application

* Adjust worker thread pool size
* Optimize memory limits and heap size
* Enable clustering for horizontal scaling
* Review and optimize slow database queries

#### Kubernetes Resources

* Right-size CPU and memory requests/limits
* Configure horizontal pod autoscaling (HPA)
* Implement pod disruption budgets
* Optimize persistent volume performance class

### Capacity Planning

Regular capacity planning should review:

#### Growth Trends

* User/tenant growth rate
* Flow execution volume trends
* Data storage growth

#### Resource Utilization

* Average and peak CPU/memory usage
* Database storage growth
* Network bandwidth utilization

#### Performance Baselines

* Establish performance baselines
* Track deviation from baselines
* Plan scaling activities before limits are reached

### Compliance and Security Monitoring

* Monitor access logs for suspicious activity
* Track failed authentication attempts
* Review audit logs for compliance requirements
* Track SSL certificate expiration dates

### Additional Resources

* [OpenShift Monitoring documentation](https://docs.openshift.com/container-platform/latest/monitoring/monitoring-overview.html)
* [MongoDB Operations Best Practices](https://www.mongodb.com/docs/manual/administration/production-notes/)
* [RabbitMQ Monitoring Guide](https://www.rabbitmq.com/monitoring.html)
* [Elasticsearch Monitoring and Observability](https://www.elastic.co/guide/en/elasticsearch/reference/current/monitor-elasticsearch-cluster.html)


# Getting Started

If you successfully installed the Appmixer self-managed package, you should be able to open the Appmixer front-end application at <http://localhost:8080>. You should see the sign-in page:

![Sign-in page](/files/-L_r83nl6ii1q5bqal_J)

Click on the "Sign up" button to create a new account:

![Sign-up page](/files/-L_r8L0f4hYFwBoHhb77)


# Authentication Hub

Appmixer features an internal Authentication Hub, functioning as an authentication proxy, which simplifies the setup process by eliminating the need to register your own OAuth credentials with third-party services. This means all OAuth-based connectors offered by Appmixer are ready to use right out of the box. However, should you prefer to use your own OAuth credentials for enhanced customization or compliance reasons, you have the flexibility to do so. This can be done for all or selected connectors by specifying your OAuth credentials (`clientId` and `clientSecret`) as outlined in the [Connector Configuration](/getting-started/services) section of this documentation.

The Authentication Hub is enabled for all hosted Appmixer tenants. In self-managed installations, the Authentication Hub has to be explicitly set.

## Basic Setup

To start using the Authentication Hub, the system plugin `auth-hub` has to be turned on. This is automatically turned on for Hosted Appmixer tenants and can be turned on by adding the `auth-hub` plugin ID to the `SYSTEM_PLUGINS` ENV variable.

```sh
# Turning on the plugin
SYSTEM_PLUGINS=auth-hub

# Required variables
AUTH_HUB_URL=             # URL of the Authentication Hub
AUTH_HUB_TOKEN={token}    # Provided by Appmixer

# Optional variables
AUTH_HUB_AUTOMATIC=true|false    # true by default

```

To request the `AUTH_HUB_TOKEN`, contact our customers support by sending email to [support@appmixer.com](https://mail:support@appmixer.com).

## Additional Configuration

You can redirect all OAuth requests to the Authentication Hub, or you can use it just for a selected set of connectors.

If the `AUTH_HUB_AUTOMATIC` is set to `true` then all OAuth requests for all installed connectors that do not have clientId/clientSecret (OAuth 2), or consumerKey/consumerSecret (OAuth 1) configured through the Backoffice (Connector Configuration), will be redirected to the Authentication Hub. This is useful, if you want to use all the available connectors, but don't want to create all the necessary OAuth apps.

If the `AUTH_HUB_AUTOMATIC` is set to `false`, Appmixer checks the connector configuration. If the connector has clientId/clientSecret (OAuth 2), or consumerKey/consumerSecret (OAuth 1) configured through the Backoffice (Connector Configuration), Appmixer will not use the Authentication Hub (all OAuth requests will go directly from your Appmixer tenant to the relevant OAuth 3rd party). If you add `authHubUrl` to the connector configuration, all the authentication requests will be redirected to the Authentication Hub URL provided:

<figure><img src="/files/Vn4iCyT11DTGtOwzGLR4" alt=""><figcaption></figcaption></figure>




---

[Next Page](/llms-full.txt/1)

