Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...

https://api.YOUR_TENANT.appmixer.cloud$ curl -XPOST "https://api.YOUR_TENANT.appmixer.cloud/user/auth" \
-H "Content-type: application/json" \
-d '{ "username": "abc@example.com", "password": "abc321" }'$ curl "https://api.YOUR_TENANT.appmixer.cloud/flows" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cC..."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.
[
{
"serviceId": "appmixer:google",
"clientID": "my-global-client-id",
"clientSecret": "my-global-client-secret"
},
{
"serviceId": "appmixer:evernote",
"sandbox": true,
}
]System configuration. The following endpoints are only accessible to users with `admin` scope.
[
{
"key": "JWTSecret",
"value": "OQekJ3DH4pRnWFl4wlN0hzhc5UIjdihEwFnwYLYUdXGXk+/f5JieT/1VLPUJnvALIGK014md41rUuarqYZscl2T5azHQmFhQmUKj8dEuoIELWB45wlkxDKcojCQi9Otk76itnmvKrbm/ZokDJxePNv2Edgc7/mLrTHG7l54w44c="
},
{
"key": "WEBHOOK_FLOW_COMPONENT_ERROR",
"value": "https://example.com/webhook"
}
]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.
{
"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": "appmixer.twitter.statuses.CreateTweet" }{ "label": "Create Tweet" }{
"icon": "data:image/svg+xml;base64,PD94bWwgdmV..."
}{
"auth": {
"service": "appmixer:google",
"scope": [
"https://mail.google.com/",
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/gmail.send"
]
}
}
{
"name": "appmixer.utils.http.WebhookWithOPTIONS",
"description": "Support for OPTIONS and POST",
"webhook": true,
"httpRequestMethods": [ "POST", "OPTIONS" ],
...
}{
"author": "David Durman <david@client.io>"
}{
"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"
}
}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.
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 });<!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>{
"user": {
"id": "5c88c7cc04a917256c726c3d",
"username":"abc@example.com",
"email": "abc@example.com"
},
"token":"eyJhbGciOiJIUzI1NiIsInR5cC..."
}{
"serviceId": "appmixer:google",
"clientID": "my-global-client-id",
"clientSecret": "my-global-client-secret"
}{
"serviceId": "appmixer:google",
"clientID": "my-global-client-id",
"clientSecret": "my-global-client-secret"
}{
"serviceId": "appmixer:google",
"clientID": "my-global-client-id",
"clientSecret": "my-global-client-secret"
}{}{
"key": "myConfigKey",
"value": "My Custom Value"
}{ "ok": true }{ "ok": true }{
"quota": {
"manager": "pipedrive",
"resources": "requests",
"scope": {
"userId": "{{userId}}"
}
}
}{
"quota": {
"manager": "your-service",
// Before the quota request is created, the system will check the user's
// metadata.tier value. If set, it will be used as a 'resources' value.
// If not, the value 'basic' will be used.
"resources": "{{userMetadata.tier || 'basic'}}",
"scope": {
"userId": "{{userId}}"
}
}
}module.exports = {
rules: [
{
name: 'basic-tier',
limit: 10, // 10 requests per minute
window: 1000 * 60, // 1 minute
throttling: 'window-sliding',
queueing: 'fifo',
resource: 'basic', // the 'basic' resource
scope: 'userId'
},
{
name: 'paid-tier',
limit: 100, // or 100 requests per minute
window: 1000 * 60, // 1 minute
throttling: 'window-sliding',
queueing: 'fifo',
resource: 'paid', // the 'paid' resource
scope: 'userId'
}
]
};{
"quota": {
"manager": "your-service",
// Before the quota request is created, the system will check the user's
// account profileInfo.tier value. If set, it will be used as a 'resources' value.
// If not, the value 'basic' will be used.
"resources": "{{profileInfo.tier || 'basic'}}",
"scope": {
"userId": "{{userId}}"
}
}
}{
"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"
}
}
}<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>import { Appmixer } from './appmixer.es.js'
import './appmixer.css'
const appmixer = new Appmixer(/* ... */)import { Designer, FlowManager } from './appmixer.es.js'
appmixer.ui('Designer', Designer)
appmixer.ui('FlowManager', FlowManager)
const designer = appmixer.ui.Designer(/* ... */)
const flowManager = appmixer.ui.FlowManager(/* ... */)const appmixer = new Appmixer({/* [name]: value */})
appmixer.set(name, value)
appmixer.get(name)appmixer.ui('Widget', {/* ... */})
appmixer.ui.Widget({/* ... */})appmixer.set(key, value)appmixer.get(key, value)appmixer.registerCustomComponentShape(name, shape)appmixer.registerInspectorField(type, Field, options)const connectors = appmixer.ui.Connectors(config)
connectors.set(key, value)
connectors.get(key)connectors.state(name, value)const connectors = appmixer.ui.Connectors({
el: '#connectors'
})
connectors.open()const files = appmixer.ui.Files(config)
files.set(key, value)
files.get(key)files.state(name, value)// Set a custom query.
files.state('query', {
pattern: 'my custom pattern',
sort: { uploadDate: -1 }
});
// Listen for query changes triggered by user interaction.
files.on('change:query', query => {
console.log('Current query:', query);
});files.on(event, handler)files.on('flow:open', flowId => {/* ... */})const files = appmixer.ui.Files({
el: '#files'
})
files.open()/* 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
});
}
}
});{
"title": CHATGPT_INFERED_USER_STORY_TITLE,
"content": CHATGPT_INFERED_USER_STORY_DESCRIPTION_IN_MARKDOWN
}You are a helpful assistant specializing in Product Insights. As an AI assistant you analyze support tickets to identify patterns and generate product improvement suggestions. You have access to two data sources: a support tickets database containing customer feedback and issues, a product teams directory with information about team responsibilities.
When responding to queries, first analyze the ticket data to identify common themes. Then, when asked for recommendations, match issues to the appropriate product teams, and generate specific, actionable product improvement suggestions. For each suggestion, identify the responsible team lead's email address, format your suggestion professionally, and include data-backed reasoning. Always be concise and specific in your responses, providing relevant statistics from the ticket data and clear implementation recommendations.
Finally, when asked to send an email with recommendations to a team lead, send the generated suggestions to the team lead of the relevant product team. Always reply confirming that you have sent the email and say to which email address you have sent it.Send emails to the product team.Read the support tickets submitted by users last month.Read the scopes and responsbilities and contact details of product teams.Manage the modules available in the system.
appmixer.api.sendAppEvent('contact-created', {
email: 'david@example.com',
fname: 'David',
lname: 'Doe'
});curl -XPOST \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer VIRTUAL_USER_ACCESS_TOKEN" \
-d '{ "email": "david@example.com", "fname": "David", "lname": "Doe" }' \
"https://APPMIXER_TENANT_API_URL/plugins/appmixer/utils/appevents/events/contact-created"Appmixer allows you to upload files to use them in your flows.
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).
{
"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]}}}"
}
}
}
}
}
}
}
}
{
"description": "This action gets the current weather conditions for a location."
}{
"marker": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL..."
}{
"firePatterns": ['*', 1]
}{
"firePatterns": [1, 1]
}{
"firePatterns": [
['*', 1],
[1, 0]
]
}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'
}
]
};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.
Browse logs of messages that passed through flows.
Create charts to visualize logs of messages that passed through flows.
const insightsChartEditor = appmixer.ui.InsightsChartEditor(config)
insightsChartEditor.set(key, value)
insightsChartEditor.get(key)config.el ...insightsChartEditor.state(name, value)loadererrorinsightsChartEditor.on(event, handler)closeinsightsChartEditor.on('close', () => {/* ... */})const insightsChartEditor = appmixer.ui.InsightsChartEditor({
el: '#insights-chart-editor'
})
insightsChartEditor.open()Browse and manipulate charts created by the current user.
const insightsDashboard = appmixer.ui.InsightsDashboard(config)
insightsDashboard.set(key, value)
insightsDashboard.get(key)config.el ...insightsDashboard.state(name, value)loadererrorinsightsDashboard.on(event, handler)chart:cloneinsightsDashboard.on('chart:clone', chartId => {/* ... */})chart:removeinsightsDashboard.on('chart:remove', chartId => {/* ... */})chart:openinsightsDashboard.on('chart:open', chartId => {/* ... */})const insightsDashboard = appmixer.ui.InsightsDashboard({
el: '#insights-dashboard'
})
insightsDashboard.open()Manage accounts authorized by the current user.
const accounts = appmixer.ui.Accounts(config)
accounts.set(key, value)
accounts.get(key)config.el ...accounts.state(name, value)loadererroraccounts.on(event, handler)flow:openaccounts.on('flow:open', flowId => {/* ... */})const accounts = appmixer.ui.Accounts({
el: '#accounts'
})
accounts.open()Manage records associated with data storage utility components of flows.
Manage tasks created by utility components of flows.
Manage a flow that is used as an integration instance.
const wizard = appmixer.ui.Wizard(config)
wizard.set(key, value)
wizard.get(key)config.el ...config.flowIdwizard.state(name, value)loadererrorwizard.on(event, handler)flow:startwizard.on('flow:start', flowId => {/* ... */})flow:validationwizard.on('flow:validation', errors => {/* ... */})cancelwizard.on('cancel', () => {/* ... */})closewizard.on('close', () => {/* ... */})const wizard = appmixer.ui.Wizard({
el: '#wizard',
flowId: 'your-integration-id'
})
wizard.on('flow:start', async flowId => {
await appmixer.api.startFlow(flowId)
wizard.close()
})
wizard.open()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.
var appmixer = new Appmixer({ baseUrl: BASE_URL });
appmixer.set('strings', STRINGS);appmixer.set('strings', {
ui: {
flowManager: {
search: 'Search flows',
header: {
buttonCreateFlow: 'Create new Flow'
}
}
}
});wget https://my.appmixer.com/appmixer/package/strings-en.json
[
{
"role": "admin",
"resource": "*",
"action": [
"*"
],
"attributes": [
"non-private"
]
},
{
"role": "user",
"resource": "*",
"action": [
"*"
],
"attributes": [
"non-private"
]
},
{
"role": "tester",
"resource": "*",
"action": [
"*"
],
"attributes": [
"non-private"
]
}
]{
"user": {
"id": "5c88c7cc04a917256c726c3d",
"username":"abc@example.com",
"isActive": false,
"email": "abc@example.com",
"plan":"free"
},
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
}['*', 'flows']['*', 'read', '!read', 'create', '!create', 'update', '!update', 'delete', '!delete']{
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
}{
"error": "Too many signup attempts for this email address. Please try again later."
}{
"error": "Too many signup attempts from your IP address. Please try again later."
}{
"id": "58593f07c3ee4f239dc69ff7",
"username": "tomas@client.io",
"isActive": true,
"email": "tomas@client.io",
"scope": [
"user"
],
"plan": "beta"
}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'
}]
};widget.open()widget.close()widget.reload()widget.reset()widget.state(path, value) // setter
widget.state(path) // getterwidget.set(key, value)widget.get(key, value)widget.on(name, handler)widget.off(name){
"foo": false,
"bar": { "counter": 1 }
}// set properties by key or path
widget.state('foo', true)
widget.state('bar', { counter: 2 })
widget.state('bar/counter', 3)
// get properties by key or path
widget.state('foo') // true
widget.state('bar') // { counter: 3 }
widget.state('bar/counter') // 3
// get the entire state
widget.state() // { foo: true, bar: { counter: 3 } }
// reset the state to defaults
widget.reset() // { foo: false, bar: { counter: 1 } }{
"name": "[vendor].[service]",
"label": "My App Label",
"category": "applications",
"categoryIndex": 2,
"index": 1,
"description": "My App Description",
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD...."
} {
"name": "[vendor].[service].[module]",
"label": "My App Label",
"category": "applications",
"categoryIndex": 2,
"index": 3,
"description": "My App Description",
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD...."
} callbackUrl (redirect URL). This flexibility allows you to customize the authentication process to better fit your application's workflow.{
"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."
}
}{
"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"
{
"length": 146737,
"chunkSize": 261120,
"uploadDate": "2020-07-24T20:23:38.565Z",
"filename": "chart1.png",
"md5": "1d0ed5eb2cacbab4526de272837102dd",
"metadata": {
"userId": "5f15d59cef81ecb3344fab55"
},
"contentType": "image/png",
"fileId": "8cb8fed0-0cd8-4478-8372-9f7cb4eb16e3"
}[
{
"length": 2366210,
"chunkSize": 261120,
"uploadDate": "2022-09-14T15:25:33.281Z",
"filename": "3mb.pdf",
"md5": "69604bdd54ff681ecd0bf166544c0854",
"metadata": {
"userId": "6123bbeb34598f20b676833b"
},
"contentType": "application/pdf",
"fileId": "7b917904-41b8-4c66-9f5f-0c1bf897eab6"
}
]{
count: 2
}{
// Response
}{
// Response
}nullconst insightsLogs = appmixer.ui.InsightsLogs(config)
insightsLogs.set(key, value)
insightsLogs.get(key)// Initialize with histogram hidden
const insightsLogs = appmixer.ui.InsightsLogs({
options: { showHistogram: false }
});
// Later: show histogram dynamically
insightsLogs.set('options', {
...insightsLogs.get('options'), // keep other options unchanged
showHistogram: true
});insightsLogs.state(name, value)const insightsLogs = appmixer.ui.InsightsLogs({
state: { filterLayout: 'collapsed' }
});
insightsLogs.state('filterLayout', 'expanded');// Initial query: last 30 days
const insightsLogs = appmixer.ui.InsightsLogs({
state: {
query: {
query: {
range: {
from: {
endOf: null,
startOf: 'day',
subtract: [30, 'day']
}
}
}
}
}
});// Get the current query
const currentQuery = insightsLogs.state('query');
// Merge in new filter values
insightsLogs.state('query', {
...currentQuery, // keep existing top-level query state
query: {
...currentQuery.query, // keep existing nested filters
targets: {
// Keys are Flow IDs
// Values are optional arrays of component IDs
// (empty array = all components in that flow)
'8a47ab76-b90c...': ['component-1', 'component-2'],
'f0e1d2c3-b90c...': [] // no component filter
}
}
});insightsLogs.on('change:query', queryAfterUserInput => {
console.log(queryAfterUserInput);
});const currentFlowType = widget.state('query/flowType');
// flowType filter
widget.state('query/flowType', 'automation'); // single value
widget.state('query/flowType', [
'integration-test',
'integration-instance',
'automation'
]); // multiple values
// userId filter
widget.state('query/userId', 'A'); // single value
widget.state('query/userId', ['A', 'B']); // multiple valuesconst insightsLogs = appmixer.ui.InsightsLogs({
el: '#insights-logs'
})
insightsLogs.open()appmixer.set('strings', {
time: {
months: [...],
monthsShort: [...],
weekDaysShort: [...],
ordinal(number){ ... },
relativeTime: {...}
}
});ordinal(number) {
const b = number % 10;
const output = (~~(number % 100 / 10) === 1) ? 'th'
: (b === 1) ? 'st'
: (b === 2) ? 'nd'
: (b === 3) ? 'rd' : 'th';
return number + output;
}appmixer.set('strings', {
ui: {
flowManager: {
pagination: '{{range}} of {{total}} flow|{{range}} of {{total}} flows'
}
}
});

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






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
















appmixer.ui.AutomationHub({
/* ... */
options: {
header: {
tabs: {
hidden: ['logs', 'accounts']
}
}
}
})appmixer.ui.AutomationHub({
/* ... */
state: {
tab: 'logs'
}
})automationHub.state(name, value)automationHub.on(event, handler)automationHub.on('flow:open-designer', ({ data }) => {/* ... */})automationHub.on('flow:open-wizard', ({ data }) => {/* ... */})automationHub.on('flow:create-custom', ({ data, next }) => {/* ... */})automationHub.on('flow:start', ({ data, next }) => {/* ... */})automationHub.on('flow:stop', ({ data, next }) => {/* ... */})automationHub.on('flow:clone', ({ data, next }) => {/* ... */})automationHub.on('flow:rename', ({ data, next }) => {/* ... */})automationHub.on('flow:remove', ({ data, next }) => {/* ... */})automationHub.on('flow:insights-logs', ({ data, next }) => {/* ... */})const designer = appmixer.ui.Designer({ el: '#designer' })
const automationHub = appmixer.ui.AutomationHub({
el: '#automation-hub',
options: {
customization: {
entryPoints: {
templates: true,
scratch: true
}
}
}
})
automationHub.on('flow:open-designer', ({ data }) => {
designer.set('flowId', data.flow.flowId)
designer.open()
})
automationHub.on('flow:open-wizard', ({ data }) => {
const wizard = appmixer.ui.Wizard({
el: '#wizard',
flowId: data.flow.flowId
})
wizard.open()
})
automationHub.open()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.jsonappmixer pack appmixer/mcpservers/your_mcp_server
appmixer publish appmixer.mcpservers.your_mcp_server.zip
$ npm install -g appmixer$ appmixer url https://api.[YOURTENANT].appmixer.cloud$ appmixer login your@admin.com
Password:$ appmixer init component appmixer.boredapi.core.GetActivity \
--description "Get a random activity to do when I am bored." \
--author "Appmixer Team <info@appmixer.com>" \
--inPorts in \
--outPorts out \
--iconUri "https://cdn.iconscout.com/icon/free/png-256/free-bored-267462.png" \
--serviceLabel BoredAPI \
--serviceDescription "Get random activities."$ tree appmixer
appmixer
└── boredapi
├── core
│ └── GetActivity
│ ├── GetActivity.js
│ ├── component.json
│ └── package.json
└── service.json
3 directories, 4 files{
"name": "appmixer.boredapi.core.GetActivity",
"description": "Get a random activity to do when I am bored.",
"icon": "data:image/png;base64,iVBORw0KGgoAA...",
"author": "Appmixer Team <info@appmixer.com>",
"inPorts": [{
"name": "in",
"schema": {
"type": "object",
"properties": {}
},
"inspector": {
"inputs": {}
}
}],
"outPorts": [{
"name": "out",
"options": []
}]
}{
"name": "appmixer.boredapi.core.GetActivity",
"description": "Get a random activity to do when I am bored.",
"icon": "data:image/png;base64,iVBORw0KGgoAA...",
"author": "Appmixer Team <info@appmixer.com>",
"inPorts": [{
"name": "in",
"schema": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"education", "recreational", "cooking"
]
}
}
},
"inspector": {
"inputs": {
"type": {
"type": "select",
"index": 0,
"label": "Type",
"tooltip": "Type of the activity",
"options": [
{ "content": "Education", "value": "education" },
{ "content": "Recreational", "value": "recreational" },
{ "content": "Cooking", "value": "cooking" }
]
}
}
}
}],
"outPorts": [{
"name": "out",
"options": [
{ "label": "Activity", "value": "activity" },
{ "label": "Accessibility", "value": "accessibility" },
{ "label": "Type", "value": "type" },
{ "label": "Participants", "value": "participants" },
{ "label": "Price", "value": "price" }
]
}]
}module.exports = {
receive: async function(context) {
let url = 'http://www.boredapi.com/api/activity';
url += '?type=' + context.messages.in.content.type;
const { data } = await context.httpRequest({ url: url, method: 'GET' });
return context.sendJson(data, 'out');
}
};$ appmixer test component appmixer/boredapi/core/GetActivity \
-i '{ "in": {"type": "recreational"} }'$ appmixer pack appmixer/boredapi # generates appmixer.boredapi.zip
$ appmixer publish appmixer.boredapi.zip
$ appmixer component ls | grep boredapi # optionally list all components
appmixer.boredapi.core.GetActivity{
"categories": {
"object": {
"label": "Object",
"index": 1
},
"list": {
"label": "List",
"index": 2
},
...
},
"modifiers": {
"g_stringify": {
"name": "stringify",
"label": "Stringify",
"category": [
"object",
"list"
],
"description": "Convert an object or list to a JSON string.",
"arguments": [
{
"name": "space",
"type": "number",
"isHash": true
}
],
"returns": {
"type": "string"
},
"helperFn": "function(value, { hash }) {\n\n return JSON.stringify(value, null, hash.space);\n }"
},
"g_length": {
"name": "length",
"label": "Length",
"category": [
"list",
"text"
],
"description": "Find length of text or list.",
"arguments": [],
"returns": {
"type": "number"
},
"helperFn": "function(value) {\n\n return value && value.hasOwnProperty('length') ? value.length : 0;\n }"
},
...
}
}{}4{
"statusCode": 400,
"error": "Bad Request",
"message": "The function returned \"undefined\""
}{
"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 }"
},
...
}
}[
{
"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",
{
"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...
}{}{}{
"components": {
City: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[name]}}}
Humidity: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.humidity]}}}
Pressure: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.pressure]}}}
Temperature: {{{$.a0828f32-34b8-4c8d-b6b3-1d82ca305921.weather.[main.temp]}}}{
...
"inPorts": [
{
"name": "in",
"schema": {
"type": "object"
},
"source": {
// The ListColumns component can return an array of columns in a
// Worksheet.
"url": "/component/appmixer/google/spreadsheets/ListColumns?outPort=out",
"data": {
// The ListColumns component needs two properties in order
// to get the list of columns, the Spreasheet Id and the
// Worksheet Id. Both will be taken from properties of the
// CreateRow component (the caller).
"properties": {
// Appmixer will replace 'properties/sheetId' with
// the actual value before making the call
"sheetId": "properties/sheetId",
"worksheetId": "properties/worksheetId"
},
// A transformer function 'columnsToInspector' from the
// ListColumns.js will be executed in order to transform a list
// of columns to the Appmixer Inspector.
"transform": "./ListColumns#columnsToInspector"
}
}
}
]
}"inspector": {
"inputs": {
"boardId": {
"type": "select",
"label": "Board",
"index": 1,
"source": {
"url": "/component/appmixer/trello/list/ListBoards?outPort=boards",
"data": {
"transform": "./transformers#boardsToSelectArray"
}
},
"tooltip": "Select a board."
},
"boardListId": {
"type": "select",
"label": "Board list",
"index": 2,
"source": {
"url": "/component/appmixer/trello/list/ListBoardsList?outPort=lists",
"data": {
"messages": {
"in/boardId": "inputs/in/boardId",
"in/isSource": true
},
"transform": "./transformers#boardListsToSelectArray"
}
},
"tooltip": "Select a list."
},
...
}
}...
"inspector": {
"inputs": {
"inputWithoutAuth": {
"type": "select",
"label": "Input without Auth",
"index": 2,
"source": {
"url": "/component/appmixer/test/staticAuth/StaticAuth?outPort=out&ignoreAuth=true",
"data": {
"transform": "./StaticAuth#getOutputOptions"
}
}
}
}
}
......
"inspector": {
"inputs": {
"inputWithSilentAuth": {
"type": "select",
"label": "Input with Silent Auth",
"index": 1,
"source": {
"url": "/component/appmixer/test/staticAuth/StaticAuth?outPort=out&silentAuth=true",
"data": {
"transform": "./StaticAuth#getOutputOptions"
}
}
}
}
}
...{
"variablesPipeline": {
"scopeDepth": 1,
"rawValue": true
}
}





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















[
{
"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
}
]curl -XGET "http://[API-URL]/categories" \
-H "Authorization: Bearer [ACCESS_TOKEN]"curl -XGET "http://[API-URL]/categories/5f8a7b2c3d4e5f6a7b8c9d0e" \
-H "Authorization: Bearer [ACCESS_TOKEN]"{
"id": "5f8a7b2c3d4e5f6a7b8c9d0e",
"name": "CRM",
"description": "Customer relationship management integrations",
"created": 1678901234567,
"mtime": 1678901234567
}nullcurl -XPOST "http://[API-URL]/categories" \
-H "Authorization: Bearer [ADMIN_TOKEN]" \
-H "Content-type: application/json" \
-d '{ "name": "CRM", "description": "Customer relationship management integrations" }'{
"id": "5f8a7b2c3d4e5f6a7b8c9d0e",
"name": "CRM",
"description": "Customer relationship management integrations",
"created": 1678901234567,
"mtime": 1678901234567
}{
"statusCode": 400,
"error": "Bad Request",
"message": "Category with this name already exists."
}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" }'{
"id": "5f8a7b2c3d4e5f6a7b8c9d0e",
"name": "CRM & Sales",
"description": "Customer relationship management and sales tools",
"created": 1678901234567,
"mtime": 1678905678901
}{
"statusCode": 400,
"error": "Bad Request",
"message": "Category with this name already exists."
}{
"statusCode": 404,
"error": "Not Found",
"message": "Category not found."
}curl -XDELETE "http://[API-URL]/categories/5f8a7b2c3d4e5f6a7b8c9d0e" \
-H "Authorization: Bearer [ADMIN_TOKEN]"{
"success": true
}{
"statusCode": 404,
"error": "Not Found",
"message": "Category not found."
}curl -XPOST "http://[API-URL]/flows" \
-H "Authorization: Bearer [ACCESS_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"name": "Salesforce to HubSpot Sync",
"type": "integration-template",
"categories": ["5f8a7b2c3d4e5f6a7b8c9d0e", "5f8a7b2c3d4e5f6a7b8c9d0f"],
"flow": { ... }
}'curl -XPUT "http://[API-URL]/flows/[FLOW_ID]" \
-H "Authorization: Bearer [ACCESS_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"categories": ["5f8a7b2c3d4e5f6a7b8c9d0e"]
}'curl -XGET "http://[API-URL]/flows/[FLOW_ID]" \
-H "Authorization: Bearer [ACCESS_TOKEN]"{
"id": "796d7b5c-bea0-4594-a9df-a8a0e3c4616e",
"name": "Salesforce to HubSpot Sync",
"type": "integration-template",
"categories": ["5f8a7b2c3d4e5f6a7b8c9d0e", "5f8a7b2c3d4e5f6a7b8c9d0f"],
"flow": { ... },
...
}# 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"]
}'{
"statusCode": 400,
"error": "Bad Request",
"message": "Category with ID invalid-category-id does not exist."
}# 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{
"appmixer.asana": {
{
"appmixer.asana": { "version": "1.2.0" },
"appmixer.calendly": { "version": "2.0.1" },
"appmixer.dropbox": { "version": "1.5.3" }
}[
{
"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
}
}
}
}
}
}
}
}
][
"appmixer.asana.projects.CreateProject",
"appmixer.asana.projects.NewProject",
"appmixer.asana.tasks.CreateStory",
"appmixer.calendly.events.InviteeCanceled",
"appmixer.calendly.events.InviteeCreated",
"appmixer.clearbit.enrichment.FindCompany",
"appmixer.clearbit.enrichment.FindPerson"
][
{
"name": "appmixer.asana.tasks.CreateStory",
"author": "David Durman <david@client.io>",
"description": "Create a story (comment) on a task.",
"icon": "https://api.appmixer.com/icons/appmixer.asana.tasks.CreateStory",
"auth": { "service": "appmixer:asana" },
"inPorts": [ /* ... */ ],
"outPorts": [ /* ... */ ],
"properties": { /* ... */ }
}
// ... one object per component
](binary zip archive){
"error": "There is nothing that corresponds to appmixer.myservice."
}{
"ticket":"a194d145-3768-4a8a-84a4-4f1e4e08c4ad"
}// Successful upload. No errors:
{
"finished": "2020-02-28T15:57:34.549Z"
}
// Upload finished. Errors encountered:
{
"finished": "2020-02-28T15:25:39.515Z",
"err": "Invalid schema for appmixer/utils/service.json",
"data": [
{
"keyword": "required",
"dataPath": "",
"schemaPath": "#/required",
"params": {
"missingProperty": "label"
},
"message": "should have required property 'label'"
},
{
"keyword": "required",
"dataPath": "",
"schemaPath": "#/required",
"params": {
"missingProperty": "description"
},
"message": "should have required property 'description'"
}
]
}{
"error": "There's no upload under ticket 2e9dd726-2b7f-46f7-bea4-8db7f7175aa8"
}{}schema property to each option, which contains a JSON Schema definition. For example:{
"outPorts": [
{
"name": "weather",
"options": [
{ "label": "Temperature", "value": "main.temp" },
{ "label": "Pressure", "value": "main.pressure" },
{ "label": "Humidity", "value": "main.humidity" },
{ "label": "Sunrise time (unix, UTC)", "value": "sys.sunrise" },
{ "label": "Sunset time (unix, UTC)", "value": "sys.sunset" },
{ "label": "City name", "value": "name" },
{ "label": "Weather description", "value": "weather[0].description" },
{ "label": "Weather icon code", "value": "weather[0].icon" },
{ "label": "Weather icon URL", "value": "weather[0].iconUrl" }
]
}
]
}{
"outPorts": [
{
"name": "weather",
"options": [
{ "label": "Temperature", "value": "main.temp" },
{ "label": "Pressure", "value": "main.pressure" },
{ "label": "Humidity", "value": "main.humidity" },
{ "label": "Sunrise time (unix, UTC)", "value": "sys.sunrise" },
{ "label": "Sunset time (unix, UTC)", "value": "sys.sunset" },
{ "label": "City name", "value": "name" },
{
"label": "Weather data",
"value": "weather",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string", "title": "Weather description" },
"icon": { "type": "string", "title": "Weather icon code" },
"iconUrl": { "type": "string", "title": "Weather icon URL" }
}
}
}
}
]
}
]
}{
"outPorts": [
{
"name": "weather",
"schema": {
"type": "object",
"properties": {
{ "title": "Temperature", "value": "main.temp" },
{ "title": "Pressure", "value": "main.pressure" },
{ "title": "Humidity", "value": "main.humidity" },
{ "title": "Sunrise time (unix, UTC)", "value": "sys.sunrise" },
{ "title": "Sunset time (unix, UTC)", "value": "sys.sunset" },
{ "title": "City name", "value": "name" },
{
"label": "Weather data",
"value": "weather",
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string", "title": "Weather description" },
"icon": { "type": "string", "title": "Weather icon code" },
"iconUrl": { "type": "string", "title": "Weather icon URL" }
}
}
}
}
}
}
}
]
}{
"outPorts": [
{
"name": "out",
"source": {
// We will call another component to construct the output port
// options, in this case, the GetRows component
"url": "/component/appmixer/google/spreadsheets/GetRows?outPort=out",
// Every Appmixer component can have 'properties' and input ports,
// the 'data' sections is used to create the input data object
// for the component
"data": {
// in this particular case, the GetRows component has an
// optional property called 'generateOutputPortOptions', we
// will pass that property with the value 'true'. The GetRows
// component will use this property to change its return value
// and instead of returning rows from Worksheet, it will
// return the 'options' array.
"properties": {
"generateOutputPortOptions": true
},
// the GetRows component expects the Spreadsheet ID and
// Worksheet ID as part of the message at its input port
// called 'in'. The UpdatedRow component is a trigger, it
// does not have an input port, but it has the same options like
// 'allAtOnce', 'withHeaders', ... and since it does not have
// an input port, it has these options defined in the
// 'properties' section. The next 'messages' section is used
// to construct an input port object for the GetRows component.
// It copies the user defined properties from the UpdatedRow.
// Appmixer will replace these with the actual values before
// calling the GetRows component.
"messages": {
"in/sheetId": "properties/sheetId",
"in/worksheetId": "properties/worksheetId",
"in/allAtOnce": "properties/allAtOnce",
"in/withHeaders": "properties/withHeaders",
"in/rowFormat": "properties/rowFormat"
}
}
}
}
]
}
appmixer.ui.Integrations({
/* ... */
options: {
customFilter: {
// displaying Integrations with a certain customField
'customFields.category': 'healthcare',
// that are shared with someone
'sharedWith': '![]'
// and in this example, that are shared throught the 'domain'
// options with a 'testdomain.com'
'sharedWith.domain': 'testdomain.com'
// or we could filter only Integrations that are shared with
// scope 'user'
// 'sharedWith.scope': 'user',
}
}
}appmixer.ui.Integrations({
/* ... */
options: {
customFilter: [
{ 'customFields.category': 'your-category-for-templates' },
{ 'customFields.category': 'your-category-for-instances' }
]
}
});// Get the current widget options.
const currentOptions = integrations.get('options') || {};
// Override the custom filter and keep the rest of the options unchanged.
integrations.set('options', {
...currentOptions,
customFilter: {
'customFields.category': 'finance'
}
});
// Reload the widget to apply the updated filter.
integrations.reload();integrations.state(name, value)integrations.on(event, handler)integrations.on('integration:create', integrationId => {/* ... */})integrations.on('integration:edit', integrationId => {/* ... */})integrations.on('integration:remove', integrationId => {/* ... */})integrations.on('integration:start', integrationId => {/* ... */})integrations.on('integration:stop', integrationId => {/* ... */})const integrations = appmixer.ui.Integrations({
el: '#integrations'
})
integrations.open()
integrations.on('integration:create', async templateId => {
// Create integration flow as a clone of the template. Disconnect
// accounts because they might not be shared with the end user.
const integrationId = await appmixer.api.cloneFlow(templateId, { connectAccounts: false })
await appmixer.api.updateFlow(integrationId, { templateId })
const wizard = appmixer.ui.Wizard({
el: '#your-wizard-div',
flowId: integrationId,
});
wizard.open()
integrations.reload()
})
integrations.on('integration:edit', function(integrationId) {
var wizard = appmixer.ui.Wizard({
el: '#wizard',
flowId: integrationId
});
wizard.open()
});
integrations.open()



<script src="https://TENANT_ID.appmixer.ai/appmixer/package/appmixer.js"></script>const appmixer = new Appmixer({ baseUrl: 'https://api-TENANT_ID.appmixer.ai' });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);
}
}










API for users
Access Data Stores (built-in key-value store).
API for user groups (workspaces)
Control the charts in a user dashboard (Insights UI).
Appmixer lets you manage the components' inspector fields through the manifest or the strings object.








































{
"user": {
{
"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVjODhjN2NjMDRhOTE3MjU2YzcyNmMzZCIsInNjb3BlIjpbInVzZXIiXSwiaWF0IjoxNTUyNDkyNjA5LCJleHAiOjE1NTUwODQ2MDl9.9jVcqY0qo9Q_1GeK9Fg14v7OrdpWvzmqnv4jDMZfqnI"
}{
"id": "58593f07c3ee4f239dc69ff7",
"username": "tomas@client.io",
"isActive": true,
"email": "tomas@client.io",
"scope": [
"user"
],
"metadata": {},
"plan": "beta"
}{
"ticket": "830639e3-c53a-42d6-ad43-0276674236b4"
}{
"status": "in-progress | completed | failed | cancelled",
"stepsDone": 4, // can be used to display a progress bar
"stepsTotal": 10 // can be used to display a progress bar
}[
{
"messageId": "a9b78d3c-ec9a-4c0e-81c2-b1df12bd46d7",
"flowId": "796d7b5c-bea0-4594-a9df-a8a0e3c4616e",
"componentId": "fdb29d7b-c6b7-423b-adb2-87b41289e925",
"messages": {
"in": [
{
"properties": {
"correlationId": "0dcb7b2a-5933-481a-bb9c-c08a865656c0",
"gridInstanceId": null,
"contentType": "application/json",
"contentEncoding": "utf8",
"sender": {
"componentId": "3961d498-83f8-4714-85ba-0539d3055892",
"type": "appmixer.utils.controls.OnStart",
"outputPort": "out"
},
"destination": {
"componentId": "fdb29d7b-c6b7-423b-adb2-87b41289e925",
"inputPort": "in"
},
"correlationInPort": null,
"componentHeaders": {},
"flowId": "796d7b5c-bea0-4594-a9df-a8a0e3c4616e",
"messageId": "12374d7e-5c66-40d1-8772-37c424bd4182",
"flowRunId": 1603726768950
},
"content": {
"key": "hello"
},
"scope": {
"3961d498-83f8-4714-85ba-0539d3055892": {
"out": {
"started": "2020-10-26T15:39:29.003Z"
}
}
},
"originalContent": {
"started": "2020-10-26T15:39:29.003Z"
}
}
]
},
"created": "2020-10-26T15:39:29.097Z",
"err": //Stringyfied error object...
}
]{}curl "https://api.appmixer.com/stores/5c6fc9932ff3ff000747ead4" -H "Authorization: Bearer [ACCESS_TOKEN]"{
"name": "My Store 1
{
"count": 681
}[{
"key":"Box 8RE1",
"storeId":"5b214ba6f90a6200113abfd8",
"userId":"583c06511afb7b0016ef120b",
"updatedAt":"2019-03-06T10:02:20.419Z",
"value":"321",
"createdAt":"2019-03-06T10:02:20.419Z"
},{
"key":"T-shirt T41B",
"storeId":"5b214ba6f90a6200113abfd8",
"userId":"583c06511afb7b0016ef120b",
"updatedAt":"2019-03-06T10:01:59.360Z",
"value":"18",
"createdAt":"2019-03-06T10:01:59.360Z"
},{
"key":"T-shirt A12C",
"storeId":"5b214ba6f90a6200113abfd8",
"userId":"583c06511afb7b0016ef120b",
"updatedAt":"2019-03-06T10:01:45.204Z",
"value":"12",
"createdAt":"2019-03-06T10:01:45.204Z"
}]{
"storeId": "5c7f9bfe51dbaf0007f08db0"
}{
"oldName":"My Old Store Name",
"storeId":"5c7f9bfe51dbaf0007f08db0"
}{
"key":"mykey",
"value":"myvalue",
"createdAt":"2019-03-06T10:17:58.796Z",
"updatedAt":"2019-03-06T10:17:58.796Z"
}{
"key": "New Key",
"value": "New Value"
"createdAt": "2021-09-01T11:34:00.258+0000",
"updatedAt": "2021-09-01T11:34:00.258+0000",
}{
"deletedCount": 1
}{
// Response
}[{
"name": "My Store 1",
"storeId": "5c6fc9932ff3ff000747ead4"
}, {
"name": "My Store 2",
"storeId": "2a3fc9512bb3fca23747lai2"
}]{
"id": "5f8a7b2c3d4e5f6a7b8c9d0e",
"name": "Marketing Team",
"userId": "group-1678901234567",
"metadata": {
"department": "marketing"
},
"created": 1678901234567,
"members": []
}[
{
"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"]
}
]{
"id": "5f8a7b2c3d4e5f6a7b8c9d0e",
"name": "Marketing Team",
"userId": "group-1678901234567",
"metadata": {
"department": "marketing"
},
"created": 1678901234567,
"members": ["user1", "user2", "user3"]
}{
"id": "5f8a7b2c3d4e5f6a7b8c9d0e",
"name": "Marketing & Sales Team",
"userId": "group-1678901234567",
"metadata": {
"department": "marketing",
"region": "EMEA"
},
"created": 1678901234567
}{
"success": true
}{
"added": ["user1", "user2", "user3"],
"group": {
"id": "5f8a7b2c3d4e5f6a7b8c9d0e",
"name": "Marketing Team",
"members": ["user1", "user2", "user3"]
}
}{
"success": true,
"removedUserId": "user1",
"revokedTokens": 3
}{
"members": [
{
"id": "user1",
"username": "john@example.com",
"email": "john@example.com"
},
{
"id": "user2",
"username": "jane@example.com",
"email": "jane@example.com"
}
]
}{
"groups": [
{
"id": "5f8a7b2c3d4e5f6a7b8c9d0e",
"name": "Marketing Team",
"userId": "group-1678901234567",
"metadata": {
"department": "marketing"
}
},
{
"id": "5f8a7b2c3d4e5f6a7b8c9d0f",
"name": "Admin Group",
"userId": "group-1678901234568",
"metadata": {}
}
]
}{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"context": {
"type": "group",
"groupId": "5f8a7b2c3d4e5f6a7b8c9d0e",
"groupName": "Marketing Team",
"originalUserId": "user1"
},
"user": {
"id": "group-1678901234567",
"username": "group-1678901234567",
"scope": ["user"],
"type": "group"
}
}{
"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"
}
}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" } }'curl -XGET "https://api.appmixer.com/user-groups" \
-H "Authorization: Bearer [ACCESS_TOKEN]"curl -XGET "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e" \
-H "Authorization: Bearer [ACCESS_TOKEN]"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" } }'curl -XDELETE "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e" \
-H "Authorization: Bearer [ADMIN_TOKEN]"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"] }'curl -XDELETE "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e/members/user1" \
-H "Authorization: Bearer [ADMIN_TOKEN]"curl -XGET "https://api.appmixer.com/user-groups/5f8a7b2c3d4e5f6a7b8c9d0e/members" \
-H "Authorization: Bearer [ACCESS_TOKEN]"curl -XGET "https://api.appmixer.com/users/user1/groups" \
-H "Authorization: Bearer [ACCESS_TOKEN]"curl -XPOST "https://api.appmixer.com/auth/switch-context" \
-H "Authorization: Bearer [ACCESS_TOKEN]" \
-H "Content-type: application/json" \
-d '{ "groupId": "5f8a7b2c3d4e5f6a7b8c9d0e" }'curl -XGET "https://api.appmixer.com/auth/available-contexts" \
-H "Authorization: Bearer [ACCESS_TOKEN]"{
chartId: '5defb3901f17d98d974fbb00'
}{
chartId: "5defa30cbd1ca06288202346"
index: 1
mtime: "2019-12-10T13:52:12.288Z"
name: "Updated Chart"
options: {,…}
query: {,…}
traces: {,…}
type: "bar"
userId: "5dee76c19462fe6b3fd42d79"
}[
{
"chartId": "5defa30cbd1ca06288202346",
"userId": "5dee76c19462fe6b3fd42d79",
"name": Chart 1",
"index": 0,
"type": "bar",
"query": { ... },
"options": { ... },
"traces": { ... },
"mtime": "2019-12-10T13:52:12.288Z"
},
{
"chartId": "5defa30cbd1ca06288202347",
"userId": "5dee76c19462fe6b3fd42d79",
"name": Chart 2",
"index": 0,
"type": "bar",
"query": { ... },
"options": { ... },
"traces": { ... },
"mtime": "2019-13-10T13:52:12.288Z"
}
]{
"chartId": "5defa30cbd1ca06288202346",
"userId": "5dee76c19462fe6b3fd42d79",
"name": Chart 1",
"index": 0,
"type": "bar",
"query": { ... },
"options": { ... },
"traces": { ... },
"mtime": "2019-12-10T13:52:12.288Z"
}{
"name":"Test create chart",
"index":1,
"type":"bar",
"options":{
"layout":{
"showlegend":true,
"xaxis":{
"showline":true
},
"yaxis":{
"showline":false
},
"barmode":"group",
"bargap":0.2,
"bargroupgap":0.1
},
"showgrid":true,
"showticklabels":true,
"horizontal":false
},
"query":{
"range":{
"from":{
"endOf":null,
"startOf":"day",
"subtract":[7,"day"]
},
"to":{}
}
},
"traces":{
"2f985875-4149-4c7b-a4ab-e204269c0c0f":{
"name":"Trace 1",
"hidden":false,
"agg":{
"date_histogram":{
"interval":"1d",
"min_doc_count":"0",
"field":"@timestamp"
}
},
"options":{
"type":"bar",
"linewidth":0,
"opacity":1
},
"source":{
"type":"flow",
"targets":{
"dbd206a4-23b3-44a4-a6c4-59db74aa3fb5":[]
}
}
}
}
}{
"name": "appmixer.twilio.sms.SendSMS",
"author": "David Durman <david@client.io>",
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVp...",
"description": "Send SMS text message through Twilio.",
"private": false,
"auth": {
"service": "appmixer:twilio"
},
"outPorts": [
{
"name": "sent",
"options": [
{ "label": "Message Sid", "value": "sid" }
]
}
],
"inPorts": [
{
"name": "message",
"schema": {
"type": "object",
"properties": {
"body": { "type": "string" },
"to": { "type": "string" },
"from": { "type": "string" }
},
"required": [
"from", "to"
]
},
"inspector": {
"inputs": {
"body": {
"type": "text",
"label": "Text message",
"tooltip": "Text message that should be sent.",
"index": 1
},
"from": {
"type": "select",
"label": "From number",
"placeholder": "Type number",
"tooltip": "Select Twilio phone number.",
"index": 2,
"source": {
"url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
"data": {
"transform": "./transformers#fromNumbersToSelectArray"
}
}
},
"to": {
"type": "text",
"label": "To number",
"tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
"index": 3
}
}
}
}
],
"localization": {
"cs": {
"label": "Pošli SMS",
"description": "Pošli SMS pomocí Twilia",
"inPorts[0].name": "Zpráva",
"inPorts[0].inspector.inputs.body.label": "Textová zpráva",
"inPorts[0].inspector.inputs.from.label": "Číslo volajícího",
"inPorts[0].inspector.inputs.from.placeholder": "Hledej číslo",
"outPorts[0].name": "Odesláno",
"outPorts[0].options[sid].label": "Sid zprávy"
},
"sk": {
"label": "Pošli SMS",
"description": "Pošli SMS pomocou Twilia",
"inPorts[0].name": "Správa",
"inPorts[0].inspector.inputs.body.label": "Textová správa",
"inPorts[0].inspector.inputs.from.label": "číslo volajúceho",
"outPorts[0].name": "Odoslané",
"outPorts[0].options[sid].label": "Sid správy"
}
}
}// Create an SDK instance
var appmixer = new Appmixer()
// Will use the strings under 'cs' key
appmixer.set('lang', 'cs')
// Will switch the strings to the ones under 'sk' key
appmixer.set('lang', 'sk')var appmixer = new Appmixer();
var mySkStrings = { /* Strings definition for sk language */ };
var myCsStrings = { /* Strings definition for cs language */ };
// This function will be called when the user clicks on some
// "Switch to sk" button
function setLangToSk() {
appmixer.set('lang', 'sk');
appmixer.set('strings', mySkStrings);
}
// This function will be called when the user clicks on some
// "Switch to cs" button
function setLangToCs() {
appmixer.set('lang', 'cs');
appmixer.set('strings', myCsStrings);
}{
"components": {
"appmixer.twilio.sms.SendSMS": {
"inPorts[0].inspector.inputs.body.label": "Textová zpráva",
"inPorts[0].inspector.inputs.from.label": "Číslo volajícího",
"inPorts[0].inspector.inputs.from.placeholder": "Hledej číslo"
}
}
// Other namespaces (designer, storage, accounts...)
}{
"name": "appmixer.twilio",
"label": "Twilio",
"category": "applications",
"description": "Twilio is an easy tool for developers to send and receive SMS and voice calls.",
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMj...",
"localization": {
"cz": {
"label": "Modul Twilio",
"description": "Twilio je snadný nástroj pro vývojáře k odesílání a přijímání SMS a hlasových hovorů."
},
"sk": {
"label": "Modul Twilio",
"description": "Twilio je ľahký nástroj pre vývojárov na odosielanie a prijímanie SMS a hlasových hovorov."
}
}
}"appmixer.twilio": {
"label": "Modul Twilio",
"description": "Twilio je snadný nástroj pro vývojáře k odesílání a přijímání SMS a hlasových hovorů."
}appmixer.set('strings', {
ui: {
designer: {
stencil: {
groups: {
applications: 'Connectors',
utilities: 'Tools'
}
}
}
}
});function generateSecureUsertoken(length = 22) {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(crypto.getRandomValues(new Uint32Array(length)))
.map((x) => charset[x % charset.length])
.join('');
}
generateSecureUsertoken() // ODQMwnwGeZQeXTV5sj3AsR<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Appmixer Automation Hub – Minimal Demo</title>
</head>
<body>
<div id="widget"></div>
<script src="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><div id="integrations-placeholder"></div>const integrations = appmixer.ui.Integrations({
el: '#integrations-placeholder',
options: {
showHeader: true
}
});
const wizard = appmixer.ui.Wizard();
integrations.on('integration:create', templateId => {
wizard.close();
wizard.set('flowId', templateId);
wizard.open();
});
integrations.on('integration:edit', integrationId => {
wizard.close();
wizard.set('flowId', integrationId);
wizard.open();
});
wizard.on('flow:start-after', () => integrations.reload());
wizard.on('flow:remove-after', () => {
integrations.reload();
wizard.close();
});
integrations.open();<div id="flow-manager-placeholder"></div>
<div id="designer-placeholder"></div>const automations = appmixer.ui.FlowManager({
el: '#flow-manager-placeholder',
options: {
menu: [{ event: 'flow:remove', label: 'Remove' }]
}
});
const designer = appmixer.ui.Designer({
el: '#designer-placeholder',
options: {
showButtonHome: true,
menu: [
{ event: 'flow:rename', label: 'Rename' }
],
toolbar: [
['undo', 'redo'],
['zoom-to-fit', 'zoom-in', 'zoom-out'],
['logs']
]
}
});
automations.on('flow:open', flowId => {
designer.close();
designer.set('flowId', flowId);
designer.open();
});
designer.on('navigate:flows', () => {
designer.close();
automations.reload();
});
automations.open();appmixer.set('strings', {
ui: {
flowManager: {
search: 'Search Automations',
header: {
title: 'Automations',
buttonCreateFlow: 'Create Automation'
}
}
}
});appmixer.set('theme', {
mode: 'light',
ui: {
shapes: {
action: 'action-vertical',
trigger: 'trigger-vertical'
}
},
variables: {
font: {
family: 'serif',
familyMono: 'monospace',
size: 16
},
colors: {
background: '#FFFFFF',
surface: '#FFFDFC',
separator: '#493843',
neutral: '#493843',
primary: '#493843',
secondary: '#61988E',
tertiary: '#EABDA8',
error: '#B3261E',
warning: '#B56C09',
success: '#08B685',
modifier: '#C558CF',
highlighter: '#FFA500'
},
corners: {
elementRadiusSmall: '0px',
elementRadiusMedium: '0px',
elementRadiusLarge: '0px',
containerRadiusSmall: '0px',
containerRadiusMedium: '0px',
containerRadiusLarge: '0px'
},
dividers: {
regular: '2px',
medium: '4px',
semibold: '6px',
bold: '6px',
extrabold: '9px'
}
}
});
Customize UI widgets. You can change the colors, the typography, and much more.
const appmixer = new Appmixer({
theme: {
variables: {
font: {
family: "sans-serif"
},
colors: {
neutral: "orange"
}
}
}
});const flowManager = appmixer.ui.FlowManager({
el: "#my-flow-manager",
theme: {
variables: {
colors: {
neutral: "purple"
}
}
}
});// change the theme of all widgets
appmixer.set('theme', {
variables: {
font: {
family: 'serif'
}
}
})
// or/and change the theme of a single widget
widget.set('theme', {
variables: {
colors: {
neutral: 'green'
}
}
});appmixer.set('theme', {
mode: 'light', // Determines the color mode of the theme: 'light' or 'dark'.
variables: {
// Font variables including font family, weights, and size.
font: {
family: '\'SF Pro Text\', \'Helvetica Neue\', \'Helvetica\', \'Arial\', sans-serif',
familyMono: '\'SF Mono\', \'ui-monospace\', Menlo, monospace',
weightRegular: 400,
weightMedium: 500,
weightSemibold: 600,
weightBold: 700, // Added key for bold font weight.
size: 14
},
// Color variables for various UI elements, with light and dark mode defaults.
colors: {
background: '#FFFFFF', // Color of the background. The areas accomodate surfaces.
surface: '#FFFFFF', // Color of the surfaces above background and other surfaces.
separator: '#E0E0E2', // Separator is a special color for various borders and lines.
neutral: '#1F2338',
primary: '#2A64F6', // Colors for primary, secondary and tertiary actions of the user.
secondary: '#6B7EB3',
tertiary: '#8C6C87',
error: '#B3261E',
warning: '#B56C09',
success: '#08B685',
modifier: '#C558CF', // Special color for variables that have been modified via Modifiers.
highlighter: '#FFA500'
},
// Shadow variables for different elevation levels and UI elements.
shadows: {
level0: '0 0 4px 0 rgba(60, 64, 67, 0.3)', // Shadows from level 0 to 5 serve to assist with elevation levels between surfaces.
level1: '0 0 1px rgb(125 125 126)',
level2: '0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.12)',
level3: '0 2px 4px 0 rgba(60, 64, 67, 0.3), 0 2px 6px 1px rgba(60, 64, 67, 0.12)',
level4: '0 3px 9px 0 rgba(60, 64, 67, 0.15), 0 3px 12px 1px rgba(60, 64, 67, 0.12)',
level5: '0 4px 8px 0 rgba(60, 64, 67, 0.3), 0 4px 12px 1px rgba(60, 64, 67, 0.12)',
backdrop: 'rgba(0 0 0 / 88%)', // Backdrop is a special shadow to cover the background of modals and popups.
blur: 'rgba(0 0 0 / 10%)', // Blur is an alternative for "backdrop" when the background is blurred instead.
popover: '0 3px 9px rgba(0 0 0 / 12%)', // Popover is a special shadow for popups that should be at the maximum elevation.
icon: '0 1px 3px rgb(0 0 0 / 6%)'
},
// Radius variables for element and container corners.
corners: {
elementRadiusSmall: '3px',
elementRadiusMedium: '6px',
elementRadiusLarge: '9px',
elementRadiusRound: '300px',
containerRadiusSmall: '3px',
containerRadiusMedium: '6px',
containerRadiusLarge: '9px'
},
// Border width variables for UI elements.
dividers: {
regular: '1px', // Width of border lines and separator lines in the UI.
medium: '2px',
semibold: '3px',
bold: '6px',
extrabold: '9px'
}
}
})The Appmixer SDK uses this API module internally to connect to the REST API.
Browse and manipulate flows that are accessible to the current user.
originalUserId: The actual user ID (for audit logging)




await appmixer.api.authenticateUser(username, password)await appmixer.api.authenticateWithEmailAndPassword(email, password)await appmixer.api.signupUser(username, password, [email])await appmixer.api.createFlow(name, [descriptor], [properties])await appmixer.api.deleteFlow(flowId)await appmixer.api.getFlow(flowId)await appmixer.api.getFlows(query){
limit: 20,
offset: 0,
pattern: "slack",
projection: "-flow,-thumbnail",
sort: "mtime:-1",
sharedWithPermission: "read",
filter: "userId:423jfdsalfjl4234fdsa"
}await appmixer.api.getFlowsCount(query)await appmixer.api.updateFlow(flowId, update)await appmixer.api.startFlow(flowId)await appmixer.api.stopFlow(flowId)await appmixer.api.cloneFlow(flowId)await appmixer.api.getUser()await appmixer.api.getStores()await appmixer.api.getStore(storeId)await appmixer.api.getStoreRecordsCount(query)await appmixer.api.getStoreRecords(query){
limit: 30,
offset: 0,
pattern: "foo",
sort: "updatedAt:-1",
storeId: “5c6d643f4849f447eba55c1d"
}await appmixer.api.createStore(name)await appmixer.api.deleteStore(storeId)await appmixer.api.renameStore(storeId, newName)await appmixer.api.createStoreItem(storeId, key, value)await appmixer.api.deleteStoreItems(items)await appmixer.api.createAccount(params, data)await appmixer.api.getAccounts(filter)await appmixer.api.getComponentAccounts(componentType, componentId)await appmixer.api.getAccountFlows(accountId)await appmixer.api.setAccountName(accountId, newName)await appmixer.api.getLogs(query){
from: 0,
size: 30,
sort: "@timestamp:desc",
query: "@timestamp:[2018-01-01 TO 2018-01-01]"
}{
from: 0,
size: 30,
sort: "@timestamp:desc",
query: "@timestamp:[2018-01-01 TO 2018-01-01] AND +flowId:FLOW_ID"
}await appmixer.api.getLog(logId, index)await appmixer.api.getPeopleTasks(query)await appmixer.api.getPeopleTasksCount(query)await appmixer.api.getPeopleTask(id)await appmixer.api.approveTask(id, [params])await appmixer.api.rejectTask(id, [params])await appmixer.api.getCharts()await appmixer.api.getChart(chartId)await appmixer.api.deleteChart(chartId)await appmixer.api.getFlowAuthentication(flowId)appmixer.api.on(event, handler)appmixer.api.on('error', error => {
if (error.code === 401) {
/* A request failed because the current access token is invalid ... */
}
}appmixer.api.on('warning', warning => { ... }{
"buckets": [
{
"key_as_string": "2018-04-16T00:00:00.000Z",
"key": 1523836800000,
"doc_count": 35
},
{
"key_as_string": "2018-04-17T00:00:00.000Z",
"key": 1523923200000,
"doc_count": 60
}
],
"hits": [
{
"severity": "info",
"componentType": "appmixer.slack.list.SendChannelMessage",
"componentId": "a1cda3ff-8e20-41df-8e7d-8e52419e6d17",
"portType": "in",
"senderId": "c062e744-2de1-4c80-afce-713be3145315",
"@timestamp": "2018-04-06T14:02:04.517Z",
"port": "message",
"senderType": "appmixer.utils.controls.OnStart",
"correlationId": "a5128135-3a23-4837-92f8-9dc099ff0700",
"id": "339d216c-48e0-4110-9210-a4c176b30f84:a1cda3ff-8e20-41df-8e7d-8e52419e6d17:input-queue",
"gridTimestamp": "2018-04-06T14:02:04.472Z",
"flowId": "339d216c-48e0-4110-9210-a4c176b30f84",
"entity": "input-queue",
"_id": "AWKbQ6Vr9I6rzDWu4NbG",
"_index": "appmixer-201804"
},
{
"severity": "info",
"componentType": "appmixer.slack.list.SendChannelMessage",
"componentId": "a1cda3ff-8e20-41df-8e7d-8e52419e6d17",
"portType": "in",
"senderId": "c062e744-2de1-4c80-afce-713be3145315",
"@timestamp": "2018-04-03T20:22:10.971Z",
"port": "message",
"senderType": "appmixer.utils.controls.OnStart",
"correlationId": "7ed0bbb4-0b05-4469-8168-401cd909e5d2",
"id": "339d216c-48e0-4110-9210-a4c176b30f84:a1cda3ff-8e20-41df-8e7d-8e52419e6d17:input-queue",
"gridTimestamp": "2018-04-03T20:22:10.927Z",
"flowId": "339d216c-48e0-4110-9210-a4c176b30f84",
"entity": "input-queue",
"_id": "AWKNLJEg9I6rzDWu3F8E",
"_index": "appmixer-201804"
}
]
}{
"_index": "appmixer-201804",
"_type": "engine",
"_id": "AWKbQ6Vr9I6rzDWu4NbG",
"_version": 1,
"_source": {
"severity": "info",
"msg": {
"text": "Hey Slack!"
},
"componentType": "appmixer.slack.list.SendChannelMessage",
"componentId": "a1cda3ff-8e20-41df-8e7d-8e52419e6d17",
"bundleId": "86a83327-1b13-4cab-a7cd-bbcce5f2402d",
"portType": "in",
"senderId": "c062e744-2de1-4c80-afce-713be3145315",
"@timestamp": "2018-04-06T14:02:04.517Z",
"port": "message",
"@version": "1",
"senderType": "appmixer.utils.controls.OnStart",
"correlationId": "a5128135-3a23-4837-92f8-9dc099ff0700",
"id": "339d216c-48e0-4110-9210-a4c176b30f84:a1cda3ff-8e20-41df-8e7d-8e52419e6d17:input-queue",
"gridTimestamp": "2018-04-06T14:02:04.472Z",
"flowId": "339d216c-48e0-4110-9210-a4c176b30f84"
}
}{
"aggregations": {
"avg_price": {
"value": 10
},
"sum_income": {
"value": 2000
}
},
"hits": [
{ "flowId": "78230318-37b8-40ac-97a5-996ba9a6c48f", ... },
{ "flowId": "78230318-37b8-40ac-97a5-996ba9a6c48f", ... },
...
]
}{
"messageCounts": {
"from": "2018-03-17",
"to": "2018-04-17",
"count": 348,
"userId": "58593f07c3ee4f239dc69ff7"
},
"runningFlows": {
"userId": "58593f07c3ee4f239dc69ff7",
"count": 4
},
"activeConnectors": {
"userId": "58593f07c3ee4f239dc69ff7",
"count": 8
},
"usedApps": [
"appmixer.utils",
"appmixer.slack",
"appmixer.asana",
"appmixer.salesforce",
"appmixer.twilio"
]
}{
"from": "2024-01-01",
"to": "2024-02-01",
"totalCount": 24,
"totalSize": 8050,
"stats": [
{
"size": 1772,
"count": 4,
"date": "2024-02"
},
{
"size": 6278,
"count": 20,
"date": "2024-01"
}
]
}{
"totalCount": 10,
"totalSize": 9220
}appmixer.set('theme', {
ui: {
shapes: {
action: "action",
trigger: "trigger",
selection: "selection"
}
}
})appmixer.set('theme', {
ui: {
charts: {
legendFontSize: "12px",
legendFontFamily: "sans-serif",
legendFontColor: "black",
tickFontSize: "black",
tickFontFamily: "monospaced",
tickFontColor: "black",
gridColor: "lightgray",
colorway: [
'#493843',
'#61988E',
'#A0B2A6',
'#CBBFBB'
]
}
}
})appmixer.ui.FlowManager({
el: '#app',
theme: {
ui: {
'#FlowManager': {
background: 'lightblue',
'#header': {
padding: '0 0 24px 0',
'#buttonCreateFlow': {
color: 'yellow',
'@hovered': {
color: 'white'
}
}
}
}
}
}
});wget https://my.appmixer.com/appmixer/package/theme-light.json
wget https://my.appmixer.com/appmixer/package/theme-dark.jsonconst flowManager = appmixer.ui.FlowManager(config)
flowManager.set(key, value)
flowManager.get(key)appmixer.ui.FlowManager({
/* ... */
options: {
menu: [
{ event: 'flow:open', label: 'Open', icon: 'data:image/svg+xml;base64,...' },
{ event: 'flow:rename', label: 'Rename', icon: 'https://www.example.com/images/image.jpg' },
{ event: 'flow:insights', label: 'Insights' },
{ event: 'flow:clone', label: 'Clone' },
{ event: 'flow:share', label: 'Share' },
{ event: 'flow:remove', label: 'Remove' },
{ event: 'my-custom-event', label: 'Custom Event' }
]
}
}appmixer.ui.FlowManager({
/* ... */
options: {
filters: [
{ property: 'stage', value: 'running', label: 'Running flows' },
{ property: 'stage', value: 'stopped', label: 'Stopped flows' },
{ property: 'sharedWith', value: 'myFlows', label: 'My flows' },
{ property: 'sharedWith', value: 'sharedWithOthers', label: 'Shared with others' },
{ property: 'sharedWith', value: 'sharedWithMe', label: 'Shared with me' }
]
}
}appmixer.ui.FlowManager({
/* ... */
options: {
customFilter: {
stage: 'running'
}
}
}appmixer.ui.FlowManager({
/* ... */
options: {
customFilter: {
'customFields.category': 'healthcare',
'customFields.template': true
}
}
}appmixer.ui.FlowManager({
/* ... */
options: {
customFilter: {
type: [ // each flow has a 'type' property
'automation',
'integration-instance'
]
}
}
});appmixer.ui.FlowManager({
/* ... */
options: {
sorting: [
{ label: 'Last Modified', property: 'mtime', value: -1 },
{ label: 'Last Created', property: 'btime', value: -1 }
]
}
}flowManager.state(name, value)flowManager.on(event, handler)flowManager.on('flow:open', flowId => {/* ... */})flowManager.on('flow:create', () => {/* ... */})flowManager.on('flow:start', flowId => {/* ... */})flowManager.on('flow:stop', flowId => {/* ... */})flowManager.on('flow:clone', flowId => {/* ... */})flowManager.on('flow:share', flowId => {/* ... */})flowManager.on('flow:rename', flowId => {/* ... */})flowManager.on('flow:remove', flowId => {/* ... */})appmixer.ui.FlowManager({
/* ... */
options: {
menu: [{ event: 'flow:share', label: 'Share' }],
// specify custom types and scopes
shareTypes: [
{ value: 'email', label: 'Email', placeholder: 'Enter an email' },
{ value: 'scope', label: 'Scope', placeholder: 'Enter a scope' },
{ value: 'domain', label: 'Domain', placeholder: 'Enter a domain' }
],
// override default permissions
sharePermissions: [
{ label: 'Read', value: 'read' },
{ label: 'Start', value: 'start' },
{ label: 'Stop', value: 'stop' }
]
}
}// create a new widget
const flowManager = appmixer.ui.FlowManager({
el: '#flow-manager',
options: {
menu: [
{ event: 'flow:open', label: 'Open' },
{ event: 'custom-event', label: 'Custom' },
{ event: 'flow:rename', label: 'Rename' },
{ event: 'flow:insights', label: 'Insights' },
{ event: 'flow:clone', label: 'Clone' },
{ event: 'flow:share', label: 'Share' },
{ event: 'flow:remove', label: 'Remove' }
],
filters: [
{ property: 'stage', value: 'running', label: 'Running flows' },
{ property: 'stage', value: 'stopped', label: 'Stopped flows' },
{ property: 'sharedWith', value: 'myFlows', label: 'My flows' },
{ property: 'sharedWith', value: 'sharedWithOthers', label: 'Shared with others' },
{ property: 'sharedWith', value: 'sharedWithMe', label: 'Shared with me' }
],
sorting: [
{ label: 'Last Modified', property: 'mtime', value: -1 },
{ label: 'Last Created', property: 'btime', value: -1 }
]
}
})
// change default layout
flowManager.state('layout', 'list')
// override a built-in event
flowManager.on('flow:create', () => {
flowManager.state('error', 'Creating a new flow overridden by a custom event handler.')
})
// load flow details with a custom event
flowManager.on('custom-event', async flowId => {
try {
flowManager.state('loader', true)
const flow = await appmixer.api.getFlow(flowId)
alert(`Flow ${flow.name} has ${Object.keys(flow.flow).length} component(s).`)
} catch (error) {
flowManager.state('error', 'Loading flow failed.')
} finally {
flowManager.state('loader', false)
}
})
// open the widget
flowManager.open(){
"err": {
"message": "Validation error on ports: in",
"code": "GRID_ERR_VAL_PORTS",
"name": "ValidationFlowError",
"stack": "ValidationFlowError: Validation error on ports: in\n at MessagesProcessor.process (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/MessagesProcessor.js:67:19)\n at Context.prepare (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/Context.js:68:31)\n at ContextHandler.createContext (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/ContextHandler.js:60:17)\n at async InputQueue.consume (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/InputQueue.js:151:23)\n at async InputQueue.onMessage (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/AMQPClient.js:378:20)"
},
"type": "component",
"flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
"flowName": "New flow",
"userId": "5f804b96ea48ec47a8c444a7",
"componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
"componentType": "appmixer.utils.email.SendEmail",
"inputMessages": {
"in": [
{
"properties": {
"correlationId": "339bc448-a806-4e61-8d38-4211fcedaf12",
"contentType": "application/json",
"contentEncoding": "utf8",
"sender": {
"componentId": "e8c581b4-9985-4f2c-bf30-895bf1d5541b",
"type": "appmixer.utils.controls.OnStart",
"outputPort": "out"
},
"destination": {
"componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
"inputPort": "in"
},
"flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
"messageId": "47293e2c-4e33-4558-8805-386de392ef04",
"flowRunId": 1607683798995
},
"content": {
"to": "2020-12-11T10:49:59.050Z"
},
"scope": {
"e8c581b4-9985-4f2c-bf30-895bf1d5541b": {
"out": {
"started": "2020-12-11T10:49:59.050Z"
}
}
},
"originalContent": {
"started": "2020-12-11T10:49:59.050Z"
}
}
]
}
}





{
"err": {
"message": "Validation error on ports: in",
"error": {
"in": [
[
{
"keyword": "format",
"dataPath": ".to",
"schemaPath": "#/properties/to/format",
"params": {
"format": "email"
},
"message": "should match format \"email\""
}
]
]
},
"code": "GRID_ERR_VAL_PORTS",
"name": "ValidationFlowError",
"stack": "ValidationFlowError: Validation error on ports: in\n at MessagesProcessor.process (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/MessagesProcessor.js:67:19)\n at Context.prepare (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/Context.js:68:31)\n at ContextHandler.createContext (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/context/ContextHandler.js:60:17)\n at async InputQueue.consume (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/InputQueue.js:151:23)\n at async InputQueue.onMessage (/Users/martinkrcmar/Programming/client/appmixer/appmixer-core/engine/src/AMQPClient.js:378:20)"
},
"type": "component",
"flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
"flowName": "New flow",
"userId": "5f804b96ea48ec47a8c444a7",
"componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
"componentType": "appmixer.utils.email.SendEmail",
"inputMessages": {
"in": [
{
"properties": {
"correlationId": "339bc448-a806-4e61-8d38-4211fcedaf12",
"contentType": "application/json",
"contentEncoding": "utf8",
"sender": {
"componentId": "e8c581b4-9985-4f2c-bf30-895bf1d5541b",
"type": "appmixer.utils.controls.OnStart",
"outputPort": "out"
},
"destination": {
"componentId": "0bb33e42-fbc4-464e-98f1-459f1ff626ac",
"inputPort": "in"
},
"flowId": "32f605b2-8fbe-4f68-9db9-ce182b35c159",
"messageId": "47293e2c-4e33-4558-8805-386de392ef04",
"flowRunId": 1607683798995
},
"content": {
"to": "2020-12-11T10:49:59.050Z"
},
"scope": {
"e8c581b4-9985-4f2c-bf30-895bf1d5541b": {
"out": {
"started": "2020-12-11T10:49:59.050Z"
}
}
},
"originalContent": {
"started": "2020-12-11T10:49:59.050Z"
}
}
]
}
}{
"err": {
"stack": "Error: getaddrinfo ENOTFOUND mandrillapp.com\n at GetAddrInfoReqWrap.onlookup [as oncomplete] (dns.js:66:26)",
"message": "getaddrinfo ENOTFOUND mandrillapp.com",
"errno": "ENOTFOUND",
"code": "ENOTFOUND",
"syscall": "getaddrinfo",
"hostname": "mandrillapp.com",
"name": "Error"
},
"type": "component",
"flowId": "0ab287ef-7bd6-4cc3-b53b-c916c857cbe7",
"flowName": "Invalid email test",
"userId": "5fd744d5e9ed7d0011ca35f9",
"componentId": "cb3f4ff5-7b6e-4d24-b7a8-2115c8254baa",
"componentType": "appmixer.utils.email.SendEmail",
"inputMessages": {
"in": [
{
"properties": {
"correlationId": "254ad628-f9c1-4483-81ed-33a22ac3ddc6",
"contentType": "application/json",
"contentEncoding": "utf8",
"sender": {
"componentId": "bcbeda1d-9036-45af-a25d-a57cf06e3f90",
"type": "appmixer.utils.controls.OnStart",
"outputPort": "out"
},
"destination": {
"componentId": "cb3f4ff5-7b6e-4d24-b7a8-2115c8254baa",
"inputPort": "in"
},
"flowId": "0ab287ef-7bd6-4cc3-b53b-c916c857cbe7",
"messageId": "33b9fcbf-b326-4eb4-bba6-cf34979f4ba2",
"flowRunId": 1607944841843,
"quotaId": "qs-4882d03d-65e1-44dc-983e-d2a33071779d"
},
"content": {
"to": [
{
"email": "martin@client.io",
"type": "to"
}
],
"from_email": "no-reply@appmixer.com"
},
"scope": {
"bcbeda1d-9036-45af-a25d-a57cf06e3f90": {
"out": {
"started": "2020-12-14T11:20:41.858Z"
}
}
},
"originalContent": {
"started": "2020-12-14T11:20:41.858Z"
}
}
]
}
}{
"event": "storage_quota_warning",
"userId": "5f804b96ea48ec47a8c444a7",
"username": "user@example.com",
"totalBytes": 94371840,
"softLimitBytes": 104857600,
"hardLimitBytes": 524288000,
"warningThreshold": 0.9,
"utilizationPercent": 90.1,
"timestamp": "2026-04-23T10:00:00Z"
}{
"event": "storage_quota_soft_exceeded",
"userId": "5f804b96ea48ec47a8c444a7",
"username": "user@example.com",
"totalBytes": 110000000,
"softLimitBytes": 104857600,
"hardLimitBytes": 524288000,
"utilizationPercent": 104.9,
"timestamp": "2026-04-23T11:00:00Z"
}{
"event": "storage_quota_hard_exceeded",
"userId": "5f804b96ea48ec47a8c444a7",
"username": "user@example.com",
"totalBytes": 530000000,
"softLimitBytes": 104857600,
"hardLimitBytes": 524288000,
"utilizationPercent": 101.1,
"blocked": true,
"timestamp": "2026-04-23T12:00:00Z"
}{
"event": "system_capacity_warning",
"physicalCapacityBytes": 107374182400,
"dataBytes": 85899345920,
"utilizationPercent": 80.0,
"maxUtilizationPercent": 95.0,
"state": "yellow",
"timestamp": "2026-04-23T10:00:00Z"
}{
"event": "system_capacity_critical",
"physicalCapacityBytes": 107374182400,
"dataBytes": 102005473075,
"utilizationPercent": 95.0,
"maxUtilizationPercent": 95.0,
"state": "red",
"blockedSubsystems": [
"inputQueue",
"dispatcher",
"webhooks",
"fileUpload",
"flowCreate",
"flowStart",
"userSignup",
"dataStore",
"polling",
"delayedMessages",
"componentTimeouts"
],
"timestamp": "2026-04-23T13:00:00Z"
}appmixer.ui.Designer({
/* ... */
options: {
validation: { show: true }
}
})appmixer.ui.Designer({
/* ... */
options: {
menu: [
{ event: 'flow:rename', label: 'Rename', icon: 'data:image/svg+xml;base64,...' },
{ event: 'flow:share', label: 'Share', icon: 'https://www.example.com/images/image.jpg' },
{ event: 'flow:wizard-builder', label: 'Wizard' },
{ event: 'flow:export-svg', label: 'Export SVG' },
{ event: 'flow:export-png', label: 'Export PNG' },
{ event: 'flow:print', label: 'Print' }
]
}
})const designer = appmixer.ui.Designer({
/* ... */
options: {
toolbar: [
['undo', 'redo'],
['zoom-to-fit', 'zoom-in', 'zoom-out'],
['logs'],
[{
tooltip: 'Reload',
widget: {
template: (
`<div @click="onClick" style="border: solid 1px gray; border-radius: 3px;">
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
</svg>
</div>`
),
methods: {
onClick() {
designer.reload()
}
}
}
}]
]
}
})const designer = appmixer.ui.Designer({
/* ... */
options: {
autoOpenLogs: true
toolbar: [
['logs']
]
}
})const designer = appmixer.ui.Designer({
/* ... */
options: {
triggerSelector: {
enabled: true,
featured: [
{
name: 'appmixer.utils.timers.Timer'
},
{
name: 'appmixer.utils.controls.OnStart',
label: 'Custom label',
description: 'Custom description',
marker: 'Custom marker text',
icon: 'data:image/svg+xml;base64,...',
}
]
}
}
})designer.state(name, value)designer.on(event, handler)designer.on('flow:start', flow => {/* ... */})designer.on('flow:stop', flow => {/* ... */})designer.on('flow:share', flow => {/* ... */})designer.on('flow:rename', flow => {/* ... */})designer.on('flow:export-svg', flow => {/* ... */})designer.on('flow:export-png', flow => {/* ... */})designer.on('flow:print', flow => {/* ... */})designer.on('flow:validation', errors => {
console.log('flow:validation', '===>', errors);
});
// Example
[
{
"keyword": "required",
"dataPath": ".text",
"schemaPath": "#/required",
"params": {
"missingProperty": "text"
},
"message": "Should have required property \"Message\".",
"schema": {
"text": {
"type": "string"
}
},
"parentSchema": {
"type": "object",
"properties": {
"text": {
"type": "string"
}
},
"required": [
"text"
]
},
"data": {
"message.d9a25ebe-84ef-4460-a061-f9acac76d28f.out.lambda": {}
},
"componentId": "d1c48d6f-0225-46a8-9600-1c19adf75768",
"descriptorPath": "config.transform.message.d9a25ebe-84ef-4460-a061-f9acac76d28f.out.lambda.text",
"fieldLabel": "Message"
}
]designer.on('flow:wizard-builder', flow => {/* ... */})designer.on('component:add', ({ data, next }) => {/* ... */})designer.on('component:open', ({ data, next }) => {/* ... */})designer.on('component:close', ({ data, next }) => {/* ... */})designer.on('component:rename', ({ data, next }) => {/* ... */})designer.on('component:update-type', ({ data, next }) => {/* ... */})designer.on('navigate:validation', (flowId) => {/* ... */})const designer = appmixer.ui.Designer({
el: '#designer',
options: {
menu: [
{ event: 'flow:rename', label: 'Rename' },
{ event: 'flow:share', label: 'Share' },
{ event: 'flow:wizard-builder', label: 'Wizard' },
{ event: 'flow:export-svg', label: 'Export SVG' },
{ event: 'flow:export-png', label: 'Export PNG' },
{ event: 'flow:print', label: 'Print' }
],
toolbar: [
['undo', 'redo'],
['zoom-to-fit', 'zoom-in', 'zoom-out'],
['logs']
]
}
})
const flowId = await appmixer.api.createFlow('New flow')
designer.set('flowId', flowId)
designer.open()














myText field inside the second group, only variableB will be available, because variableA is already been used.{
"properties: {
"inspector": {
"inputs": {
"interval": {
"type": "number",
"group": "config",
"label": "Interval (in minutes, min 5, max 35000)"
}
},
"groups": {
"config": {
"label": "Configuration",
"index": 1
}
}
}
}
}{
"type": "text",
"label": "Text message."
}{
"type": "textarea",
"label": "A multi-line text message."
}{
"type": "number",
"label": "A numerical input.",
"min": 1,
"max": 10,
"step": 1
}{
"type": "key-value",
"label": "An input field for key-value text pairs."
}{
"type": "multiselect",
"options": [
{ "content": "one", "value": 1 },
{ "content": "two", "value": 2 },
{ "content": "three", "value": 3 }
],
"placeholder": "-- Select something --",
"label": "Multi Select box"
}{
"type": "date-time",
"label": "Date",
"config": {
"enableTime": true
}
}{
"type": "toggle",
"label": "Toggle field"
}{
"type": "color-palette",
"label": "Color palette",
"options": [
{ "value": "green", "content": "Green" },
{ "value": "yellow", "content": "Yellow" },
{ "value": "orange", "content": "Orange" },
{ "value": "red", "content": "Red" },
{ "value": "purple", "content": "Purple" }
]
}{
"type": "select-button-group",
"label": "Select button group",
"options": [
{ "value": "line-through", "content": "<span style=\"text-decoration: line-through\">S</span>" },
{ "value": "underline", "content": "<span style=\"text-decoration: underline\">U</span>" },
{ "value": "italic", "content": "<span style=\"font-style: italic\">I</span>" },
{ "value": "bold", "content": "<span style=\"font-weight: bold\">B</span>" }
]
}{
"type": "select-button-group",
"label": "Select button group",
"multi": true,
"options": [
{ "value": "line-through", "content": "<span style=\"text-decoration: line-through\">S</span>" },
{ "value": "underline", "content": "<span style=\"text-decoration: underline\">U</span>" },
{ "value": "italic", "content": "<span style=\"font-style: italic\">I</span>" },
{ "value": "bold", "content": "<span style=\"font-weight: bold\">B</span>" }
]
}{
"type": "select-button-group",
"label": "Select button group with icons",
"multi": true,
"options": [
{ "value": "cloud", "icon": "data:image/png;base64,iVBORw0KGgoAA..." },
{ "value": "diamond", "icon": "data:image/png;base64,iVBORw0KGgoAAAA..." },
{ "value": "oval", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUh..." },
{ "value": "line", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." },
{ "value": "ellipse", "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEU..." }
]
}{
"type": "expression",
"label": "Filter expression",
"levels": ["OR", "AND"],
"exclusiveFields": ["myText"]
"fields": {
"myText": {
"type": "text",
"label": "Column",
"required": true,
"index": 1
},
"mySelect": {
"type": "select",
"label": "Filter action",
"variables": false,
"required": true,
"options": [
{ "content": "Equals", "value": "equals" },
{ "content": "Not Equals", "value": "notEquals" }
],
"index": 2
},
"myAnotherText": {
"label": "Filter value",
"type": "text",
"defaultValue": "My Filter",
"index": 3
}
]
}{
"OR": [
{
"AND": [
{ "myText": "My column name", "mySelect": "My filter action", "myAnotherText": "My filter value" },
{ "myText": "Another column name", "mySelect": "Another filter action", "myAnotherText": "Another filter value" }
]
},
{
"AND": [
{ "myText": "Alternative column", "mySelect": "Alternative action", "myAnotherText": "Alternative value" }
]
}
]
}{
...
"inspector": {
"inputs": {
"expressionWithSource": {
"type": "expression",
"label": "Dynamic Expression",
"tooltip": "Dynamic Expression with a <b>source</b> call.",
"exclusiveFields": [ "select" ],
"index": 1,
"levels": [ "AND", "OR" ],
"fields": {
"text": {
"type": "text",
"label": "Text",
"tooltip": "A plain text field with <b>required: true</b>. The <b>value</b> of this field will be part of the variables in the select box <b>Dynamic Select</b>. If the <b>value</b> is <b>break</b> the component will throw an error that has to be visible in the UI.",
"required": true,
"index": 1
},
"select": {
"type": "select",
"label": "Dynamic Select",
"tooltip": "Dynamic Select options have to be available.",
"index": 2,
"source": {
"url": "/component/test/test/source/ExpressionWithExpand?outPort=out",
// The "expand" value is actually a path to an array
// in the flow JSON, that array is generated by this
// "expression", the Appmixer engine will then expand
// this array and call the "source" for each item in it.
// If you have different "levels" in your expression,
// then you have to use yours here.
"expand": "$.expressionWithSource.AND.OR",
"data": {
"properties": {
"generateOptions": "select",
// it will find the correct "text" input value
// in the same "box" and use it in the "source"
// call
"text": "./text"
},
"transform": "./ExpressionWithExpand#fieldsToSelectArray"
}
}
}
}
}
}
}
...
}
"inputs": {
"fileId": {
"type": "filepicker",
"label": "Select file",
"index": 1,
"tooltip": "Pick a CSV file to import into the flow"
}
}"inputs": {
"file": {
"type": "googlepicker",
"index": 1,
"label": "File",
"placeholder": "Choose a file...",
"tooltip": "Choose a file to export."
}
}"inputs": {
"file": {
"type": "googlepicker",
"index": 1,
"label": "Folder",
"placeholder": "Choose a folder...",
"tooltip": "Choose a folder.",
"view": "FOLDERS"
}
}"input": {
"folder": {
"type": "onedrivepicker",
"index": 1,
"label": "Folder",
"placeholder": "Choose a folder...",
"tooltip": "Choose a folder to upload the file to.",
"view": "folders"
}
}"inputs": {
"field1": {
"type": "toggle",
"label": "This input controls rendering of field2",
"index": 1
},
"field2": {
"when": { "eq": { "field1": true }}
"type": "text",
"label": "This field will be only rendered if field1 is set to true",
"index": 2
}
}{
"type": "expression",
"label": "Filter expression",
"levels": ["OR", "AND"],
"fields": {
"myText": {
"type": "text",
"label": "Column",
"required": true,
"index": 1
},
"conditionalField": {
"when": { "eq": { "./myText": "Render" }}
"type": "select",
"label": "Filter action",
"variables": false,
"required": true,
"options": [
{ "content": "Equals", "value": "equals" },
{ "content": "Not Equals", "value": "notEquals" }
],
"index": 2
}
]
}{
"source": {
"url": "/component/appmixer/google/spreadsheets/ListColumns?outPort=out",
"data": {
"messages": {
"in": 1
},
"properties": {
"sheetId": "properties/sheetId",
"worksheet": "properties/worksheet"
},
"transform": "./transformers#columnsToInspector"
}
}
}{
inputs: { ... },
groups: { ... }
}module.exports.columnsToInspector = (columns) => {
let inspector = {
inputs: {},
groups: {
columns: { label: 'Columns', index: 1 }
}
};
if (Array.isArray(columns) && columns.length > 0) {
columns.forEach((column, index) => {
inspector.inputs[column[0]] = {
type: 'text',
group: 'columns',
index: index + 1
};
});
}
return inspector;
};"/component/appmixer/google/spreadsheets/ListColumns?outPort=out"{
"properties": {
"targetComponentProperty": "properties/myProperty"
}
}{
inputs: { ... },
groups: { ... }
}{
"transform": "./transformers#columnsToInspector"
}
tododemo
├── core
│ └── NewTodo
│ ├── NewTodo.js
│ └── component.json
├── auth.js
└── service.json{
"name": "appmixer.tododemo",
"label": "Todo Demo",
"description": "Appmixer Todo Demo Connector",
"category": "applications",
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQA..."
}module.exports = {
type: 'apiKey',
definition: {
auth: {
apiKey: {
type: 'text',
name: 'API Key',
tooltip: 'Your Todo app account API key. Find it in your user profile.'
}
},
validate: {
method: 'GET',
url: '{{config.baseUrl}}/me',
headers: {
'X-Api-Key': '{{apiKey}}'
}
}
}
};{
"name": "appmixer.tododemo.core.NewTodo",
"author": "Appmixer <info@appmixer.com>",
"icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQA...",
"webhook": true,
"auth": {
"service": "appmixer:tododemo"
},
"outPorts": [
{
"name": "out",
"options": [
{ "label": "Todo ID", "value": "id" },
{ "label": "Todo Label", "value": "label" }
]
}
]
}module.exports = {
async receive(context) {
// Components defined with webhook: true in the component.json file
// are eligible to receive HTTP requests to the context.getWebhookUrl().
// The headers and data of that request is available to us in the
// context.messages.webhook.content object.
if (context.messages.webhook) {
const { data } = context.messages.webhook.content;
// Remember the webhooks of the example Todo API contain a payload
// of the form { event, todo: { id, label } }. In the line below,
// we're interested only in todo-created events since our
// NewTodo component is supposed to trigger when new todo items are
// added.
if (data.event === 'todo-created') {
// If indeed a new todo item was created, send an output to the
// only component output port 'out'. The output object the
// todo object { id, label } which fields correspond to the
// 'options' list from our output port definition in component.json.
return context.sendJson(data.todo, 'out');
}
}
},
async start(context) {
// In our start() method, we register the component webhook with our
// Todo API so that we're notified of changes.
// Note the context.auth object gives us the values the user filled in
// in the authentication form defined in our tododemo service auth.js
// file.
const { apiKey } = context.auth;
// context.config object allows us to use the dynamic configuration
// from the Backoffice, the same way as we used it in our auth.js file.
const url = context.config.baseUrl + '/webhooks';
// context.getWebhookUrl() gives us the component webhook URL that
// 3rd parties (our Todo API) can call to reach our component and send
// input to it.
const { data } = await context.httpRequest.post(url, { url: context.getWebhookUrl() }, {
headers: {
'X-Api-Key': apiKey,
'Content-Type': 'application/json'
}
});
// context.saveState(myState) allows us to save a temporary state.
// This state (an arbitrary JSON object) is persisted only for the
// period the component is running (i.e. from the start of the flow
// until the flow stops. See https://docs.appmixer.com/appmixer/component-definition/behaviour#component-state
// for details.
// For our purposes, we need to store the webhook ID that we received
// from the webhook subscription endpoint so that we can later
// unsubscribe it in the stop() method below. Note that you can't
// just store the webhook ID in the NodeJS module local variable since
// it is not guaranteed by the Appmixer engine that it will keep the
// NodeJS module in memory. Moreoever, if Appmixer is running in a cluster
// environment, different nodes can execute the start() and stop() methods.
return context.saveState({ id: data.id });
},
async stop(context) {
const { apiKey } = context.auth;
const url = context.config.baseUrl + '/webhooks';
// The component state object is available to use in the context.state
// property. This property contains the object that we previously
// saved using the context.saveState(myState) method.
return context.httpRequest.delete(url + '/' + context.state.id, {
headers: {
'X-Api-Key': apiKey
}
});
}
};$ npm install -g appmixer
$ appmixer url 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.ziptwilio/
├── auth.js
├── package.json
├── service.json
└── sms
├── ListFromNumbers
│ ├── ListFromNumbers.js
│ ├── component.json
│ ├── package.json
│ └── transformers.js
└── SendSMS
├── SendSMS.js
├── component.json
└── package.jsonconst twilio = require('twilio');
module.exports = {
receive(context) {
let { accountSID, authenticationToken } = context.auth;
let client = twilio(accountSID, authenticationToken);
let message = context.messages.message.content;
return client.messages.create({
body: message.body,
to: message.to,
from: message.from
}).then(message => {
return context.sendJson(message, 'sent');
});
}
};{
"name": "appmixer.twilio.sms.SendSMS",
"author": "David Durman <david@client.io>",
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjUwMCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDI1NiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PGcgZmlsbD0iI0NGMjcyRCI+PHBhdGggZD0iTTEyNy44NiAyMjIuMzA0Yy01Mi4wMDUgMC05NC4xNjQtNDIuMTU5LTk0LjE2NC05NC4xNjMgMC01Mi4wMDUgNDIuMTU5LTk0LjE2MyA5NC4xNjQtOTQuMTYzIDUyLjAwNCAwIDk0LjE2MiA0Mi4xNTggOTQuMTYyIDk0LjE2MyAwIDUyLjAwNC00Mi4xNTggOTQuMTYzLTk0LjE2MiA5NC4xNjN6bTAtMjIyLjAyM0M1Ny4yNDUuMjgxIDAgNTcuNTI3IDAgMTI4LjE0MSAwIDE5OC43NTYgNTcuMjQ1IDI1NiAxMjcuODYgMjU2YzcwLjYxNCAwIDEyNy44NTktNTcuMjQ0IDEyNy44NTktMTI3Ljg1OSAwLTcwLjYxNC01Ny4yNDUtMTI3Ljg2LTEyNy44Ni0xMjcuODZ6Ii8+PHBhdGggZD0iTTEzMy4xMTYgOTYuMjk3YzAtMTQuNjgyIDExLjkwMy0yNi41ODUgMjYuNTg2LTI2LjU4NSAxNC42ODMgMCAyNi41ODUgMTEuOTAzIDI2LjU4NSAyNi41ODUgMCAxNC42ODQtMTEuOTAyIDI2LjU4Ni0yNi41ODUgMjYuNTg2LTE0LjY4MyAwLTI2LjU4Ni0xMS45MDItMjYuNTg2LTI2LjU4Nk0xMzMuMTE2IDE1OS45ODNjMC0xNC42ODIgMTEuOTAzLTI2LjU4NiAyNi41ODYtMjYuNTg2IDE0LjY4MyAwIDI2LjU4NSAxMS45MDQgMjYuNTg1IDI2LjU4NiAwIDE0LjY4My0xMS45MDIgMjYuNTg2LTI2LjU4NSAyNi41ODYtMTQuNjgzIDAtMjYuNTg2LTExLjkwMy0yNi41ODYtMjYuNTg2TTY5LjQzMSAxNTkuOTgzYzAtMTQuNjgyIDExLjkwNC0yNi41ODYgMjYuNTg2LTI2LjU4NiAxNC42ODMgMCAyNi41ODYgMTEuOTA0IDI2LjU4NiAyNi41ODYgMCAxNC42ODMtMTEuOTAzIDI2LjU4Ni0yNi41ODYgMjYuNTg2LTE0LjY4MiAwLTI2LjU4Ni0xMS45MDMtMjYuNTg2LTI2LjU4Nk02OS40MzEgOTYuMjk4YzAtMTQuNjgzIDExLjkwNC0yNi41ODUgMjYuNTg2LTI2LjU4NSAxNC42ODMgMCAyNi41ODYgMTEuOTAyIDI2LjU4NiAyNi41ODUgMCAxNC42ODQtMTEuOTAzIDI2LjU4Ni0yNi41ODYgMjYuNTg2LTE0LjY4MiAwLTI2LjU4Ni0xMS45MDItMjYuNTg2LTI2LjU4NiIvPjwvZz48L3N2Zz4=",
"description": "Send SMS text message through Twilio.",
"auth": {
"service": "appmixer:twilio"
},
"outPorts": [
{
"name": "sent",
"options": [
{ "label": "Message Sid", "value": "sid" }
]
}
],
"inPorts": [
{
"name": "message",
"schema": {
"type": "object",
"properties": {
"body": { "type": "string" },
"to": { "type": "string" },
"from": { "type": "string" }
},
"required": [
"from", "to"
]
},
"inspector": {
"inputs": {
"body": {
"type": "text",
"label": "Text message",
"tooltip": "Text message that should be sent.",
"index": 1
},
"from": {
"type": "select",
"label": "From number",
"tooltip": "Select Twilio phone number.",
"index": 2,
"source": {
"url": "/component/appmixer/twilio/sms/ListFromNumbers?outPort=numbers",
"data": {
"transform": "./transformers#fromNumbersToSelectArray"
}
}
},
"to": {
"type": "text",
"label": "To number",
"tooltip": "The destination phone number. <br/><br/>Format with a '+' and country code e.g., +16175551212 (E.164 format).",
"index": 3
}
}
}
}
]
}
{
"name": "appmixer.twilio.sms.SendSMS",
"version": "1.0.0",
"main": "SendSMS.js",
"author": "David Durman <david@client.io>",
"dependencies": {
"twilio": "^3.14.0"
}
}{
"name": "appmixer.twilio",
"label": "Twilio",
"category": "applications",
"description": "Twilio is a cloud communications platform as a service.",
"icon": "data:image/png;base64,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"
}const twilio = require('twilio');
module.exports = {
type: 'apiKey',
definition: () => {
return {
tokenType: 'authentication-token',
accountNameFromProfileInfo: 'accountSID',
auth: {
accountSID: {
type: 'text',
name: 'Account SID',
tooltip: 'Log into your Twilio account and find <i>API Credentials</i> on your settings page.'
},
authenticationToken: {
type: 'text',
name: 'Authentication Token',
tooltip: 'Found directly next to your Account SID.'
}
},
validate: context => {
let client = new twilio(context.accountSID, context.authenticationToken);
return client.api.accounts.list();
}
};
}
};{
"name": "appmixer.twilio",
"version": "1.0.0",
"dependencies": {
"twilio": "^3.14.0"
}
}const twilio = require('twilio');
module.exports = {
receive(context) {
let { accountSID, authenticationToken } = context.auth;
let client = twilio(accountSID, authenticationToken);
return client.incomingPhoneNumbers.list()
.then(res => {
return context.sendJson(res, 'numbers');
});
}
};{
"name": "appmixer.twilio.sms.ListFromNumbers",
"author": "David Durman <david@client.io>",
"description": "When triggered, returns a list of numbers from user.",
"private": true,
"auth": {
"service": "appmixer:twilio"
},
"inPorts": [ "in" ],
"outPorts": [ "numbers" ]
}{
"name": "appmixer.twilio.sms.ListFromNumbers",
"version": "1.0.0",
"description": "Appmixer component for twilio to get a list of phone numbers of a user.",
"main": "ListFromNumbers.js",
"author": "David Durman <david@client.io>",
"dependencies": {
"twilio": "^3.14.0"
}
}module.exports.fromNumbersToSelectArray = (numbers) => {
let transformed = [];
if (Array.isArray(numbers)) {
numbers.forEach(number => {
transformed.push({
label: number['phoneNumber'],
value: number['phoneNumber']
});
});
}
return transformed;
};





{
"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"
}{
"id": "default",
"settings": {},
"createdAt": null,
"updatedAt": null
}curl -XGET "http://[API-URL]/automation-hub/settings" \
-H "Authorization: Bearer [ACCESS_TOKEN]"curl -XPOST "http://[API-URL]/automation-hub/settings" \
-H "Authorization: Bearer [ADMIN_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"settings": {
"sharing": {
"allowedEmails": ["user@example.com", "team@company.com"]
},
"theme": {
"primaryColor": "#007bff"
}
}
}'{
"id": "default",
"settings": {
"sharing": {
"allowedEmails": ["user@example.com", "team@company.com"]
},
"theme": {
"primaryColor": "#007bff"
}
},
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-04-22T09:15:00.000Z"
}{
"statusCode": 400,
"error": "Bad Request",
"message": "\"settings.sharing.allowedEmails[0]\" must be a valid email"
}{
"statusCode": 401,
"error": "Unauthorized",
"message": "Missing authentication"
}{
"statusCode": 403,
"error": "Forbidden",
"message": "Insufficient scope"
}curl -XDELETE "http://[API-URL]/automation-hub/settings" \
-H "Authorization: Bearer [ADMIN_TOKEN]"{
"statusCode": 401,
"error": "Unauthorized",
"message": "Missing authentication"
}{
"statusCode": 403,
"error": "Forbidden",
"message": "Insufficient scope"
}curl -XPOST "http://[API-URL]/automation-hub/share/request-login" \
-H "Content-type: application/json" \
-d '{ "email": "user@example.com" }'{
"success": true,
"message": "Activation link sent to your email"
}{
"success": true,
"code": "a1b2c3d4e5f6",
"link": "https://your-hub.com/automation-hub/share/activate?code=a1b2c3d4e5f6",
"expiresIn": 3600
}{
"error": "Email not authorized. Please contact your administrator."
}{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Too many requests for this email address. Please try again in a few minutes."
}{
"error": "Failed to send activation email. Please try again."
}curl -XGET "http://[API-URL]/automation-hub/share/activate?code=a1b2c3d4e5f6"{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "507f1f77bcf86cd799439011",
"email": "user@example.com",
"username": "user@example.com"
}
}{
"error": "Activation link has already been used"
}{
"error": "Invalid or expired activation link"
}{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Too many activation attempts. Please try again in a few minutes."
}curl -XPOST "http://[API-URL]/automation-hub/share/refresh" \
-H "Content-type: application/json" \
-d '{ "email": "user@example.com" }'SMTP_HOST=smtp.example.com
SMTP_PORT=587 # Default: 587
SMTP_USER=your-username
SMTP_PASS=your-password
MAILER_FROM_NAME=Appmixer # Default: Appmixer
MAILER_FROM_EMAIL=noreply@appmixer.com # Default: noreply@appmixer.comAPPMIXER_CLOUD_EMAIL_API=https://api.appmixer.com/email
APPMIXER_CLOUD_EMAIL_API_KEY=your-api-key# Control email sending behavior
AUTOMATION_HUB_AUTO_SEND_EMAIL=true # Default: true
# Activation link TTL (time-to-live) in seconds
AUTOMATION_HUB_ACTIVATION_LINK_TTL=3600 # Default: 3600 (1 hour)
# Custom activation URL (optional)
AUTOMATION_HUB_ACTIVATE_URL=https://your-domain.com/activate
# If not set, uses: ${APPMIXER_FE_URL}/automation-hub/share/activate# Request login limits
AUTOMATION_HUB_RATE_LIMIT_REQUEST_LOGIN_EMAIL=5 # Default: 5 per minute
AUTOMATION_HUB_RATE_LIMIT_REQUEST_LOGIN_IP=20 # Default: 20 per minute
# Activate link limits
AUTOMATION_HUB_RATE_LIMIT_ACTIVATE_IP=10 # Default: 10 per minute
# Refresh link limits
AUTOMATION_HUB_RATE_LIMIT_REFRESH_EMAIL=5 # Default: 5 per minute
AUTOMATION_HUB_RATE_LIMIT_REFRESH_IP=20 # Default: 20 per minute# 1. Configure email allow list (admin)
curl -XPOST "http://[API-URL]/automation-hub/settings" \
-H "Authorization: Bearer [ADMIN_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"settings": {
"sharing": {
"allowedEmails": ["partner@company.com", "customer@example.com"]
}
}
}'
# 2. User requests access (no auth)
curl -XPOST "http://[API-URL]/automation-hub/share/request-login" \
-H "Content-type: application/json" \
-d '{ "email": "partner@company.com" }'
# 3. User receives email, clicks link and activates
curl -XGET "http://[API-URL]/automation-hub/share/activate?code=a1b2c3d4e5f6"
# Returns JWT token for marketplace accesscurl -XPOST "http://[API-URL]/automation-hub/settings" \
-H "Authorization: Bearer [ADMIN_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"settings": {
"theme": {
"primaryColor": "#007bff",
"logoUrl": "https://example.com/logo.png",
"companyName": "Acme Corp"
},
"marketplace": {
"welcomeMessage": "Welcome to our integration marketplace",
"categories": ["CRM", "Marketing", "Sales"]
}
}
}'# Get current settings
CURRENT=$(curl -s -XGET "http://[API-URL]/automation-hub/settings" \
-H "Authorization: Bearer [ADMIN_TOKEN]")
# Add new email to allow list
curl -XPOST "http://[API-URL]/automation-hub/settings" \
-H "Authorization: Bearer [ADMIN_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"settings": {
"sharing": {
"allowedEmails": [
"existing@example.com",
"new-user@example.com"
]
}
}
}'Automation hub settings updated.Automation hub settings deleted.{
"error": "Email not authorized. Please contact your administrator."
}{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Too many requests for this email address. Please try again in a few minutes."
}{
"error": "Invalid or expired activation link"
}{
"error": "Activation link has already been used"
}{
"idp_type": "oidc",
"client_id": "a361cbac-a420-42ca-8f07-fc3520d5c36b",
"client_secret": "0841fb03-55b8-4b0d-aeb0-330374348c09",
"issuer": "my-idp",
"domains": ["client.io"],
"token_url": "https://idp.example.com/auth/realms/apm/protocol/openid-connect/token",
"authorization_url": "https://idp.example.com/auth/realms/apm/protocol/openid-connect/auth",
"metadata_url": "https://idp.example.com/auth/realms/apm/.well-known/openid-configuration",
"role_mapping": {
"admin": "adm",
"user": "usr"
}
}{
"idp_type": "saml",
"client_id": "my-client-id"
"authorization_url": "https://idp.example.com/auth/realms/apm/protocol/saml",
"certificate": "MIIClTCCAX0CBgGV0[...]6smvQM7VU=",
"domains": ["localhost", "client.io"],
"role_mapping": {
"admin": "adm",
"user": "usr"
}
"default_redirect": "https://myapp.appmixer.ai/login"
} [
{
"scope": "user",
/**
* The GC will never delete files created in the past TTL (in hours). By default,
* set to 720 hours. The reason is to prevent the GC from removing files that could be needed by the
* flows.
*/
"ttl": 720, // 30 days
/**
* The user will be blocked from saving any files if the limit is reached.
*/
"hardLimit": 2000000000 // 2 GB
}
]
You can have different rules for different user scopes:
[
{ "scope": "user:, "ttl": 720, "hardLimit": 2000000000 },
{ "scope": "admin", "ttl":1440, "hardLimit": 90000000000 }
]













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























Endpoint logs return log messages with various data structures.
Authentication to apps.

{
"_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"
}{
"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"
}{
"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"
}{
"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"
}{
"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"
} "componentType": "appmixer.slack.list.SendChannelMessage",
[
{
"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"
}
] {
"accountId": "5f841f3a43f477a9fa8fa4e9",
"name": "[Name of the account]",
"displayName": null,
"service": "[vendor:service]",
"userId": "5f804b96ea48ec47a8c444a7",
"profileInfo": {
},
"pre": {},
"revoked": false
}{ "5a6e21f3b266224186ac7d04": "valid" }{ "accountId": "5abcd0ddc4c335326198c1b2" }[
{
"flowId": "9251b4b6-4cdb-42ad-9431-1843e05307be",
"name": "Flow #1"
},
{
"flowId": "777d3024-43f6-4034-ac98-1cb5f320cb3a",
"name": "Flow #2"
},
{
"flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
"name": "Flow #3"
}
]{ "ticket": "58593f07c3ee4f239dc69ff7:1d2a90df-b192-4a47-aaff-5a80bab66de5" }{
"authUrl": "https://slack.com/oauth/authorize?response_type=code&client_id=25316748213.218351034294&redirect_uri=http%3A%2F%2Flocalhost%3A2200%2Fauth%2Fslack%2Fcallback&state=38133t07c3ee4f369dc69ff7%3A1d2a90df-b192-4a47-aaff-5a80bab66de5&scope=channels%3Aread%2Cchat%3Awrite%3Auser"
}{
"accountId": "5bc0bad6f4cb78001167b173",
"tokenId": "65c49d44e49f774bb587c4e1",
"finished": true,
"updatedAt": "2024-02-08T09:22:12.496Z",
"error": null
}{ "componentId": "e25dc901-f92a-46a2-8d29-2573d4ad65e5" }{
"accountId":"5a6e21f3b266224186ac7d03",
"componentId":"e25dc901-f92a-46a2-8d29-2573d4ad65e5"
}{
"accountId": "5a6e21f3b266224186ac7d03",
"flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
"shared": [
{
"componentId": "component-1",
"componentType": "appmixer.gmail.SendEmail"
},
{
"componentId": "component-2",
"componentType": "appmixer.slack.SendMessage"
}
]
}{
"statusCode": 400,
"error": "Bad Request",
"message": "No valid tokens found for componentType: appmixer.gmail.SendEmail"
}{
"statusCode": 403,
"error": "Forbidden",
"message": "Insufficient permissions"
}{
"statusCode": 404,
"error": "Not Found",
"message": "Flow 9089f275-f5a5-4796-ba23-365412c5666e not found"
}{
"accountId": "5a6e21f3b266224186ac7d03",
"flowId": "9089f275-f5a5-4796-ba23-365412c5666e"
}{
"statusCode": 403,
"error": "Forbidden",
"message": "Insufficient permissions"
}{
"statusCode": 404,
"error": "Not Found",
"message": "Flow 9089f275-f5a5-4796-ba23-365412c5666e not found"
}// 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]'curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
"service": "appmixer:slack",
"token": {
"accessToken": "[slack access token]",
"scope": [
"channels:write",
"groups:write",
"channels:read",
"channels:history",
"groups:read",
"groups:history",
"users:read",
"chat:write:user"
]
},
"profileInfo": {
"id" : "[Name of the account that will be used in the frontend]"
}
}'curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
"service": "appmixer:google",
"token": {
"token": "[google access token]",
"expDate": "2021-02-04 15:34:48.833Z",
"refreshToken": "[google refresh token]",
"scope": [
"https://www.googleapis.com/auth/analytics",
"https://www.googleapis.com/auth/analytics.readonly",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.readonly",
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/drive.appdata",
"https://www.googleapis.com/auth/drive.file",
"https://mail.google.com/",
"https://www.googleapis.com/auth/gmail.compose",
"https://www.googleapis.com/auth/gmail.send",
"https://www.googleapis.com/auth/gmail.readonly",
"https://spreadsheets.google.com/feeds",
"profile",
"email"
]
}
}'curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
"service": "appmixer:aws",
"token": {
"accessKeyId" : "[AWS access key ID]",
"secretKey" : "[AWS secret key]"
}
}'curl --request POST 'https://api.acme.com/accounts' \
--header 'Authorization: Bearer [ACCESS_TOKEN]' \
--header 'Content-Type: application/json' \
--data-raw '{
"service": "appmixer:acme",
"token": {
"username" : "[username]",
"password" : "[password]"
}
}'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"]
}'# Template creator connects to their database
# End users don't need database credentials
curl -XPOST "http://[API-URL]/accounts/db-account-id/share" \
-H "Authorization: Bearer [ACCESS_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"flowId": "data-sync-template",
"componentIds": ["postgres-query-component"]
}'# Share premium API account with higher rate limits
# Users don't consume their own API quotas
curl -XPOST "http://[API-URL]/accounts/premium-api-account/share" \
-H "Authorization: Bearer [ACCESS_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"flowId": "analytics-template",
"componentIds": ["api-fetch-component"]
}'curl -XPOST "http://[API-URL]/accounts/5a6e21f3b266224186ac7d03/unshare" \
-H "Authorization: Bearer [ACCESS_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"flowId": "integration-template-id"
}'
[
{
"userId"
{
"userId": "58593f07c3ee4f239dc69ff7",
"flowId": "9089f275-f5a5-4796-ba23-365412c5666e",
"stage": "stopped",
"name": "Flow #4",
"btime": "2018-03-29T19:24:08.950Z",
"mtime": "2018-04-05T12:50:15.952Z",
"flow": {
"e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
"type": "appmixer.utils.http.Uptime",
"label": "Uptime",
"source": {},
"x": 110,
"y": 90,
"config": {}
},
"43f1f63a-ecd2-42dc-a618-8c96b4acc767": {
"type": "appmixer.utils.email.SendEmail",
"label": "SendEmail",
"source": {
"in": {
"e15ef119-8fcb-459b-aaae-2a3f9ee41f15": [
"up"
]
}
},
"x": 320,
"y": -10,
"config": {
"transform": {
"in": {
"e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
"up": {
"type": "json2new",
"lambda": {
"from_email": "info@appmixer.com",
"text": "Site {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.target}}} is back UP.\nDowntime: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.downTimeText}}}\nHTTP Status Code: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.statusCode}}}",
"subject": "Appmixer: Site UP ({{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.up.target}}})"
}
}
}
}
}
}
},
"416150af-b0d4-4d06-8ad1-75b17e578532": {
"type": "appmixer.utils.email.SendEmail",
"label": "SendEmail",
"source": {
"in": {
"e15ef119-8fcb-459b-aaae-2a3f9ee41f15": [
"down"
]
}
},
"x": 320,
"y": 195,
"config": {
"transform": {
"in": {
"e15ef119-8fcb-459b-aaae-2a3f9ee41f15": {
"down": {
"type": "json2new",
"lambda": {
"from_email": "info@appmixer.com",
"subject": "Appmixer: Site DOWN ({{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.target}}})",
"text": "Site {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.target}}} is DOWN.\nHTTP Status Code: {{{$.e15ef119-8fcb-459b-aaae-2a3f9ee41f15.down.statusCode}}}"
}
}
}
}
}
}
}
},
"mode": "module",
"thumbnail": "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D...",
"started": "2018-04-05T12:33:15.357Z"
}{
"count": 29
} {
"flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b"
}{
"flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b",
"result": "updated"
}{
"flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b"
}{
"cloneId": "cloned-flow-id"
}{
"cloneId": "draft-flow-id"
}{
"cloneId": "existing-draft-flow-id"
}{
"success": true,
"flowId": "original-flow-id"
}{
"statusCode": 403,
"error": "Forbidden",
"message": "You do not have permission to publish this draft."
}{
"statusCode": 404,
"error": "Not Found",
"message": "Draft not found or invalid draft type."
}{
"flowId": "26544d8c-5209-44ac-9bdf-ef786924b07b",
// equals the flowId if the background=true is sent in the query
"ticket": "26544d8c-5209-44ac-9bdf-ef786924b07b"
}{
"status": "completed",
"stepsTotal":1
}{
// Response
}{
// Response
}{
// Response
}{
// Response
}{
// Response
}{
// Response
}curl -XPOST "http://[API-URL]/flows/9089f275-f5a5-4796-ba23-365412c5666e/clone" \
-H "Authorization: Bearer [ACCESS_TOKEN]" \
-H "Content-type: application/json" \
-d '{
"setOriginFlowId": true,
"setComponentIdMap": true,
"connectAccounts": true,
"additional": {
"type": "integration-instance-draft"
}
}'curl -XPOST "http://[API-URL]/drafts/draft-flow-id/publish" \
-H "Authorization: Bearer [ACCESS_TOKEN]"{
"notes": {
"ec488c75-b240-4b8a-8939-e05ba4af32a4": {
"x": -368,
"y": -128,
"width": 432,
"height": 288,
"content": "## My note\n\nSupports **Markdown** content."
}
}
}{
"wizard": {
"fields": []
}
}{
"type": "inspectorField",
"label": "Slack Channel",
"tooltip": "The Slack channel to post into.",
"placeholder": "#general",
"source": "a0828f32.config.transform.in.76a77abf.out.lambda.channelId",
"attrs": {},
"options": {}
}{
"type": "account",
"label": "Slack account",
"tooltip": "",
"attrs": {
"service": "appmixer:slack",
"components": ["a0828f32"],
"sharedAccountId": null
},
"options": {}
}{
"type": "customField",
"label": "Customer ID",
"tooltip": "",
"placeholder": "e.g. CUST-001",
"attrs": {
"type": "text",
"path": "customFields.customerId",
"placeholder": "e.g. CUST-001",
"searchPlaceholder": null,
"service": null,
"components": null,
"options": {},
"params": {}
},
"options": {}
}{
"type": "text",
"label": "",
"tooltip": "",
"attrs": {
"format": "header",
"text": "Configure your Slack alarm"
},
"options": {}
}{
"type": "image",
"label": "",
"tooltip": "",
"attrs": {
"image": "https://example.com/wizard-banner.png"
},
"options": {}
}curl -XPOST "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": {}
}
]
}
}'GET /flows?filter=templateId:!&filter=wizard.fieldsGET /flows?filter=templateId$ appmixer -h
Usage: appmixer [options] [command]
Appmixer command line interface.
Options:
-v, --version output the version number
-h, --help output usage information
Commands:
download|d Download component.
flow|f Flow commands.
init|i Initialize component.
login|l Login into Appmixer API.
logout|o Logout from Appmixer API.
pack|p Pack component into archive.
publish|pu Publish component.
remove|rm Remove component.
test|t Test component, authentication module, ...
url|u <url> Set Appmixer API url.
help [cmd] display help for [cmd]
Go to https://docs.appmixer.com/appmixer/ to find more information.$ appmixer flow -h
Usage: appmixer flow <command>
Flow commands.
Options:
-h, --help output usage information
Commands:
start|s <flowId> Start flow.
stop|t <flowId> Stop flow.
remove|r <flowId> Remove flow.
ls|l [flowId] Ls flow.
help [cmd] display help for [cmd]$ appmixer url https://api.appmixer.com$ appmixer login david@client.io
prompt: password:
Login successful.$ appmixer init example
Usage: appmixer init example <type> [path]
Options:
--vendor [vendor] Your vendor name.
--verbosity Verbosity level of the generated output log. Number in [0 - 2] range. Defaults to 1.
-h, --help output usage information
Examples:
$ appmixer init example no-auth
Environment Variables:
AM_GENERATOR_COMPONENT_PATH Components base directory. [path] argument overrides this variable if used.$ appmixer init example no-auth
Created directory structure ./appmixer/myservice/mymodule/MyComponent
Creating component appmixer.myservice.mymodule.MyComponent
Created component manifest file ./appmixer/myservice/mymodule/MyComponent/component.json.
Created component package file ./appmixer/myservice/mymodule/MyComponent/package.json.
Created component behaviour file ./appmixer/myservice/mymodule/MyComponent/MyComponent.js.
Created service manifest file ./appmixer/myservice/service.json.
Created quota module file ./appmixer/myservice/quota.js.
Example successfully generated.
my-components
└── appmixer
└── myservice
├── mymodule
│ └── MyComponent
│ ├── MyComponent.js
│ ├── component.json
│ └── package.json
├── quota.js
└── service.json
You can now use appmixer pack appmixer/myservice && appmixer publish commands to upload it.$ appmixer pack appmixer/myservice
Packing component directory: /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice
Files found in /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice
- mymodule
- quota.js
- service.json
You are in a directory with service.json file.
I'm going to create directory structure based on name in your service.json file.
3866 total bytes
appmixer.myservice.zip$ appmixer publish appmixer.myservice.zip
Publishing archive: /Users/daviddurman/Projects/appmixer/my-components/appmixer.myservice.zip
Published.$ appmixer component ls
appmixer.actimo.contacts.CreateContact
appmixer.actimo.contacts.DeleteContact
appmixer.actimo.contacts.GetContact
appmixer.actimo.contacts.GetContacts
appmixer.actimo.contacts.UpdateContact
appmixer.actimo.groups.GetGroups
appmixer.actimo.messages.SendMessage
appmixer.apify.crawlers.Crawl
appmixer.asana.projects.CreateProject
appmixer.asana.projects.NewProject
appmixer.asana.tasks.CreateStory
appmixer.asana.tasks.CreateSubtask
appmixer.asana.tasks.CreateTask
appmixer.asana.tasks.NewComment
...appmixer remove appmixer.myservice.mymodule.MyComponentappmixer remove appmixer.myservice$ appmixer test -h
Usage: appmixer test <command>
Dev tools for testing your files.
Options:
-h, --help output usage information
Commands:
dump|d <moduleName> Get stored authentication data from previous commands.
auth|a <authModuleFile> Authenticate service.
component|c <componentFile> Test component.
help [cmd] display help for [cmd]$ appmixer test component -h
Usage: appmixer test component [options] [componentDir]
Options:
-f, --transform [transform] specify transformer
-i, --input [input] input test message object (default: [])
-m, --mime [mime] mime type, application/json by default
-p, --properties [properties] component properties (JSON format)
-s, --no-state do not show component's state
-t, --tickPeriod [tickPeriod] tick period (in ms), default is 10000 ms
-h, --help output usage information
Examples:
Following example will send input message { "to": "your@email.com" } to component's input port 'in'.
You always have to specify to which input port you want to send message.
$ appmixer test component [path-to-your-component-directory] -i '{ "in": { "to": "your@email.com" } }'
This is how to specify transformer function from transformer file.
$ appmixer test component [path-to-component] -i '{}' -f './transformers#channelsToSelectArray'
How to set properties and tick period:
$ appmixer t c [path-to-component] -p '{ "channelId: "123XYZ" }' -t 2000
You can send more than one message:
$ appmixer t c [path-to-component] -i '{ "in": { "to": "first@email.com" }}' -i '{ "in": { "to": "second@email.com" }}'
You can run appmixer command in your component's directory:
$ appmixer test c
Directory has to contain component.json file and component's source code file.
If you're developing component that needs authentication, use 'appmixer test auth' before.
If you're developing Oauth2 component you might need to refresh access token before calling this command.
Use 'appmixer test auth refresh' for such purposes.
Some of the feature (context.store, context.componentStaticCall, ... will work only if you are logged in into Appmixer.
If you want to test them in your component, call appmixer login first.
For more information, checkout our documentation at:
https://docs.appmixer.com/appmixer/component-definition/authentication
https://docs.appmixer.com/appmixer/appmixer-trial/custom-component-helloappmixer$ appmixer test component appmixer/myservice/mymodule/MyComponent -i '{ "in": { "sourceData": "foo" } }'
Testing /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice/mymodule/MyComponent
Validating properties.
Test server is listening on 2300
Starting component.
Calling receive method with input message:
in:
-
properties:
correlationId: null
gridInstanceId: null
contentType: application/json
contentEncoding: utf8
sender: null
destination: null
correlationInPort: null
componentHeaders:
signal: false
content:
sourceData: foo
scope:
{"name":"component","hostname":"MacBook-Pro.local","pid":26397,"level":30,"msg":"{\"properties\":{\"correlationId\":\"8f2ce09e-3f6d-48dd-81bd-e80189f70bb4\",\"gridInstanceId\":null,\"contentType\":\"application/json\",\"contentEncoding\":\"utf8\",\"sender\":{\"componentId\":\"70eb49e9-88df-4d0e-9549-d4e4f0f755c0\",\"type\":\"appmixer.myservice.mymodule.MyComponent\",\"outputPort\":\"out\"},\"destination\":null,\"correlationInPort\":null,\"componentHeaders\":{},\"signal\":false},\"content\":{\"sourceData\":\"foo\"},\"scope\":{\"_walkthrough\":[{\"targetId\":\"70eb49e9-88df-4d0e-9549-d4e4f0f755c0\",\"links\":[]}]}} { componentId: '70eb49e9-88df-4d0e-9549-d4e4f0f755c0',\n flowId: 'c5d05118-13b5-4ec8-a8fe-2e6ef51fdd52',\n userId: '5da735715abc4a671dfb592f',\n componentType: 'appmixer.myservice.mymodule.MyComponent',\n type: 'data',\n portType: 'out',\n port: 'out',\n inputMessages: { in: [ [Object] ] },\n annotatedMsg: { sourceData: 'foo' } }","time":"2019-10-16T15:21:22.169Z","v":0}
Component sent a message to its output port: out
{ sourceData: 'foo' }
Component's receive method finished in: 45 ms.
Return value from receive method:
undefined
Component's state at the end:
State is empty, component did not store anything into state.
Stopping component.
Destroying component.module.exports = {
receive(context) {
myBadError
context.sendJson(context.messages.in.content, 'out');
}
}$ appmixer test component appmixer/myservice/mymodule/MyComponent -i '{ "in": { "sourceData": "foo" } }'
Testing /Users/daviddurman/Projects/appmixer/my-components/appmixer/myservice/mymodule/MyComponent
Validating properties.
Test server is listening on 2300
Starting component.
Calling receive method with input message:
in:
-
properties:
correlationId: null
gridInstanceId: null
contentType: application/json
contentEncoding: utf8
sender: null
destination: null
correlationInPort: null
componentHeaders:
signal: false
content:
sourceData: foo
scope:
[ERROR]: myBadError is not defined$ appmixer download -h
Usage: appmixer download selector
Options:
-o, --out [dir] Where you want save it.
-h, --help output usage information
Examples:
Download all files for SendEmail component:
$ appmixer download vendor.google.gmail.SendEmail
Download only package.json file for SendEmail component:
$ appmixer download vendor.google.gmail.SendEmail/package.json
Download main SendEmail.js file only:
$ appmixer download vendor.google.gmail.SendEmail/SendEmail.js
Download all gmail components:
$ appmixer download vendor.google.gmail
Download auth.js file for gmail:
$ appmixer download vendor.google.gmail/auth.js
Download quota.js file for gmail:
$ appmixer download vendor.google.gmail/quota.js$ appmixer download appmixer.myservice
$ ls
appmixer.zip
$ unzip appmixer.zip
$ tree appmixer/
appmixer
└── myservice
├── mymodule
│ └── MyComponent
│ ├── MyComponent.compiled.js
│ ├── MyComponent.js
│ ├── component.json
│ ├── package-lock.json
│ ├── package.json
│ └── sourcemap-register.js
└── service.json
3 directories, 7 files$ appmixer flow ls
[Get Current Weather] : [a5769b32-8835-44ad-82e1-ece2874ea3e3] : [stopped]
[Uptime Monitor] : [5b5fd3a0-0ef2-4fc5-9a60-164f5e44c660] : [stopped]
[Daily Rainy Day Alert] : [ec1103e5-c66c-41c4-9223-029d2b328f5c] : [stopped]$ appmixer flow ls a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow: a5769b32-8835-44ad-82e1-ece2874ea3e3
Stage: stopped$ appmixer flow ls a5769b32-8835-44ad-82e1-ece2874ea3e3 --descriptor
{
"5ba2740c-929b-4599-b09e-fc8f8d5dac82": {
"type": "appmixer.utils.controls.OnStart",
"label": "OnStart",
"source": {},
"config": {},
"x": 88,
"y": 110
},
"0f366972-08fe-4cd4-80c0-227cfb6db54b": {
"type": "appmixer.utils.weather.GetCurrentWeather",
"label": "GetCurrentWeather",
"source": {
"location": {
"5ba2740c-929b-4599-b09e-fc8f8d5dac82": [
"out"
]
}
},
"config": {
"transform": {
"location": {
"5ba2740c-929b-4599-b09e-fc8f8d5dac82": {
"out": {
"type": "json2new",
"lambda": {
"city": "Prague",
"units": "metric"
}
}
}
}
}
},
"x": 286,
"y": 110
},
"ab6a22f8-916d-4aab-a5e3-2abedc03917c": {
"type": "appmixer.utils.email.SendEmail",
"label": "SendEmail",
"source": {
"in": {
"0f366972-08fe-4cd4-80c0-227cfb6db54b": [
"weather"
]
}
},
"config": {
"transform": {
"in": {
"0f366972-08fe-4cd4-80c0-227cfb6db54b": {
"weather": {
"type": "json2new",
"lambda": {
"from_email": "info@appmixer.com",
"subject": "Appmixer: Current Weather",
"text": "Temperature: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.temp}}} dgC\nPressure: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.pressure}}} hPa\nHumidity: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.main.humidity}}}%\nCloudiness: {{{$.0f366972-08fe-4cd4-80c0-227cfb6db54b.weather.clouds.all}}}%",
"to": ""
}
}
}
}
}
},
"x": 484,
"y": 110
}
} $ appmixer flow start a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow a5769b32-8835-44ad-82e1-ece2874ea3e3 successfully started.
$ appmixer flow stop a5769b32-8835-44ad-82e1-ece2874ea3e3
Flow a5769b32-8835-44ad-82e1-ece2874ea3e3 successfully stopped.$ appmixer flow remove 2058a1ee-9c19-4e94-bd7a-0da7f9bed973
Flow 2058a1ee-9c19-4e94-bd7a-0da7f9bed973 successfully removed.$ appmixer modifiers get$ appmixer modifiers publish file-with-your-modifiers.json$ appmixer modifiers delete


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
});
}
};{
receive(context) {
const smsContent = context.messages.message.content;
}
}Humidity: {{{$.ec8cd99f-0ad3-4bca-9efc-ebea5be6b596.weather.main.humidity}}}context.messages.message.content === 'Humidity: 75'const Promise = require('bluebird');
module.exports = {
async receive(context) {
// Without context.sendArray() function:
const arrayOfObjects = [
{ name: 'John', surname: 'Doe' },
{ name: 'Martin', surname: 'Tester' }
];
await Promise.map(arrayOfObjects, item => {
return context.sendJson(item, 'out');
});
// With context.sendArray() function:
await context.sendArray(arrayOfObjects, 'out');
}
}const twilio = require('twilio');
module.exports = {
type: 'apiKey',
definition() {
return {
tokenType: 'authentication-token',
accountNameFromProfileInfo: 'accountSID',
auth: {
accountSID: {
type: 'text',
name: 'Account SID',
tooltip: 'Log into your Twilio account and find <i>API Credentials</i> on your settings page.'
},
authenticationToken: {
type: 'text',
name: 'Authentication Token',
tooltip: 'Found directly next to your Account SID.'
}
},
validate: context => {
let client = new twilio(context.accountSID, context.authenticationToken);
return client.api.accounts.list();
}
};
}
};{
receive(context) {
let { accountSID, authenticationToken } = context.auth;
}
}module.exports = {
receive(context) {
const endpoint = context.config.baseUrl + '/weather';
const { data } = await context.httpRequest.get(endpoint);
// process results
}
};
{
"properties": {
"schema": {
"properties": {
"fromNumber": { "type": "string" }
}
},
"inspector": {
...
}
}
}{
receive(context) {
const fromNumber = context.properties.fromNumber;
}
}{
async receive(context) {
// Emit a message only once per day for this component instance.
const day = (new Date).getDay();
const state = context.state;
if (!state[day]) {
state[day] = true;
await context.saveState(state);
return context.sendJson({ tick: true }, 'out');
}
}
}module.exports = {
async start(context) {
// register webhook in the slack plugin
return context.service.stateAddToSet(
context.properties.channelId,
{
componentId: context.componentId,
flowId: context.flowId
}
);
},
async stop(context) {
return context.service.stateRemoveFromSet(
context.properties.channelId,
{
componentId: context.componentId,
flowId: context.flowId
}
);
}
}{
receive(context) {
// getAttachment() is an example function that retrieves a file from an API
return getAttachment(context.auth, context.messages.attachment.content.id)
.then((file) => {
return context.saveFile(file.name, file.mimeType, Buffer.from(file.data, 'base64'));
})
.then((result) => {
return context.sendJson({ fileId: result.fileId }, 'file');
});
}
}// Example
{
length: 7,
chunkSize: 261120,
uploadDate: "2023-03-22T10:34:07.751Z",
filename: "test",
md5: "3e47b75000b0924b6c9ba5759a7cf15d",
metadata: {
userId: "638f734271b34799e665c9b9",
fileId: "a1603390-1b86-4c3c-afe4-57ebb6b6c2c2",
originFlowId: "7f3f6c28-cedb-4625-9853-853e395cabf5"
},
fileId: "a1603390-1b86-4c3c-afe4-57ebb6b6c2c2"
}// Example
{
length: 7,
chunkSize: 261120,
uploadDate: "2023-03-22T10:34:07.765Z",
filename: "something.txt",
md5: "3e47b75000b0924b6c9ba5759a7cf15d",
metadata: {
userId: "638f734271b34799e665c9b9",
fileId: "1bece4a9-cdd1-457a-8e1a-401658cbc030",
originFlowId: "7f3f6c28-cedb-4625-9853-853e395cabf5"
},
fileId: "1bece4a9-cdd1-457a-8e1a-401658cbc030"
}'use strict';
module.exports = {
receive(context) {
const { fileId, content } = context.messages.in.content;
return context.replaceFileStream(
fileId,
content
).then(savedFile => {
return context.sendJson(savedFile, 'file');
});
}
};
{
"filename": "testFile",
"contentType": "text",
"length": 7,
"chunkSize": 261120,
"uploadDate": "2021-01-22T12:20:29.227Z",
"metadata": {
"userId": "5f804b96ea48ec47a8c444a7",
"fileId": "fd0e9149-3249-4d42-b519-bfd9ab6773c5"
},
"md5": "9a0364b9e99bb480dd25e1f0284c8555",
"fileId": "fd0e9149-3249-4d42-b519-bfd9ab6773c5"
}{
receive(context) {
return context.loadFile(context.messages.file.content.fileId)
.then((fileContent) => {
// uploadFileToAPI() is some function that uploads a file to an API
return uploadFileToAPI(context.auth, content.messages.file.content.fileName, fileContent);
});
}
}module.exports = {
async receive(context) {
if (context.messages.webhook) {
// Webhook URL received data.
await context.sendJson(context.messages.webhook.content.data, 'myOutPort');
// Send response to the webhook HTTP call.
// Note: you can also skip sending response immediately and send it
// in other connected components in the flow.
// If context.response() is not called, the engine waits for the first component
// that sends the response (in the same "session", i.e. the same "message flow").
return context.response('<myresponse></myresponse>', 200, { 'Content-Type': 'text/xml' });
}
// Otherwise, normal input port received data.
const input = context.messages.myInPort.content;
// The webhook URL. Do something with it (send to your API, send to other connected,
// components, send to your backend, ...)
const url = context.getWebhookUrl();
}
};
module.exports = {
async receive(context) {
const { data } = await context.httpRequest({
url: "https://some-url.com/api",
method: "POST",
data: {
username: "someuser",
password: "somepass",
},
headers: { Authentication: "some"}
});
// it is also possible to execute dedicated http methods
// see here https://axios-http.com/docs/api_intro
// for example
// context.httpRequest.get(url[, config])
const { data } = await context.httpRequest.get("https://url", { params: { name: "some" }, headers: {}});
// context.httpRequest.post(url[, data[, config]])
const { data } = await context.httpRequest.post("https://url", { username: "some" }, headers: {});
return context.response(data);
}
}await context.store.find(storeId, { query: { key: { $nin: rowIds } } })module.exports = {
async receive(context) {
// whenever there is an item added/updated/removed from the data
// store, this component will get triggered with data on the
// context.messages.webhook.content
const data = context.messages.webhook.content.data.currentValue;
if (context.messages.webhook.content.data.type === 'insert') {
await context.sendJson({
key: data.key,
storeId: data.storeId,
value: data.value,
updatedAt: data.updatedAt,
createdAt: data.createdAt
}, 'item');
}
return context.response('ok');
},
async start(context) {
// register without specifying 'events'.
await context.store.registerWebhook(context.properties.storeId);
},
async stop(context) {
await context.store.unregisterWebhook(context.properties.storeId);
}
};module.exports = {
async receive(context) {
const data = context.messages.webhook.content.data.currentValue;
await context.sendJson({
key: data.key,
storeId: data.storeId,
value: data.value,
updatedAt: data.updatedAt,
createdAt: data.createdAt
}, 'item');
return context.response('ok');
},
async start(context) {
// register only the 'insert' events on the data store.
await context.store.registerWebhook(context.properties.storeId, ['insert']);
},
async stop(context) {
await context.store.unregisterWebhook(context.properties.storeId);
}
};module.exports = {
receive(context) {
if (context.messages.timeout) {
// Timeout message.
return context.sendJson(context.messages.timeout.content, 'out');
} else {
// Normal input message.
return context.setTimeout(context.messages.in.content, 5 * 60 * 1000);
}
}
};const task = await context.callAppmixer({
endPoint: '/people-task/tasks',
method: 'POST',
body: {
title: 'My Task',
description: 'My Example Task',
requester: 'john@example.com',
approver: 'alice@example.com',
decisionBy: (function() {
const tomorrow = new Date;
tomorrow.setDate(tomorrow.getDate() + 1);
return tomorrow.toISOString();
})()
}
});module.exports = {
async receive(context) {
return context.stopFlow();
}
};'use strict';
module.exports = {
receive(context) {
const code = `
function sum(a, b) {
return a + b;
}
sum(parseInt($data.number), 100);
`;
let sum = 0;
// You can call the context.evalJavascript multiple times in receive()
for (let i = 0; i < 3; i++) {
const result = context.evalJavaScript(code, context.messages.webhook.content.data);
sum = sum + result;
}
return context.response({ result: sum });
}
};[
{
"variables": {
"dynamic": [
{
"label": "Column A",
"value": "columnA",
"componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
"port": "out"
},
{
"label": "Column B",
"value": "columnB",
"componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
"port": "out"
}
],
"static": {}
},
"sourceComponentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
"outPort": "out",
"inPort": "in"
},
{
"variables": {
"dynamic": [
{
"label": "Column C",
"value": "columnC",
"componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
"port": "out"
},
{
"label": "Column D",
"value": "columnD",
"componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
"port": "out"
}
],
"static": {}
},
"sourceComponentId": "71eb1f40-ce29-4963-8922-102739bafee4",
"outPort": "out",
"inPort": "in"
}
]{
receive(context) {
return context.loadVariables()
.then(data => {
const newSchema = data.reduce((acc, item) => {
return acc.concat(item.variables.dynamic.map(o => ({ label: o.label, value: o. value })));
}, []);
context.sendJson(newSchema, 'leftJoin');
context.sendJson(newSchema, 'innerJoin');
context.sendJson(newSchema, 'rightJoin');
});
}
}{
async start(context) {
await context.log({ test: 'my test log' });
return context.sendJson({ started: (new Date()).toISOString() }, 'out');
}
} async receive(context) {
let lock = null;
try {
lock = await context.lock(context.flowId, {
ttl: 30000,
maxRetryCount: 1
});
let { callCount = 0, messages = [] } = await context.loadState();
messages.push(context.messages.in.originalContent);
if (++callCount === context.messages.in.content.callCount) {
await context.sendJson(messages || [], 'out');
await context.saveState({ callCount });
return;
}
await context.saveState({ callCount, messages });
} finally {
if (lock) {
await lock.unlock();
}
}
}
/**
* Example of context.CancelError
*/
module.exports = {
async receive(context) {
let data = context.messages.in.content;
try {
// In this component is trying to create a record in a 3rd party
// system.
const resp = await someAPI.createSomething(data);
await context.sendJson(resp, 'out');
} catch (err) {
// And there might be a unique constraint, let's say an email
// address. And the 3rd party API will return an error with a
// message saying that this record cannot be created.
if (err.message === 'duplicate record') {
// In this case, we can tell Appmixer to cancel the message.
// Because next attempt would fail again with the same result.
throw new context.CancelError(err.message);
}
// In case of any other error, rethrow the exception. Appmixer will
// then try to process it again.
throw err;
}
}
};
// or any library you want to perform API requests
const request = require('request-promise');
module.exports = {
definition: {
// ...
// requestProfileInfo can be defined as a function. In this case, you
// can do whatever you need in here and return object with user's
// profile information (in promise)
requestProfileInfo: async context => {
// curl https://mydomain.freshdesk.com/api/v2/agents/me \
// -u myApiKey:X'
return request({
method: 'GET',
url: `https://${context.domain}.acme.com/api/v2/agents/me`,
auth: {
user: context.apiKey // 'context' will be explained later
},
json: true
});
},
// or you can specify it as an object. In this case, the object follows
// the 'request' (https://www.npmjs.com/package/request) javascript library
// options.
requestProfileInfo: {
method: 'GET',
url: 'https://acme.com/get-some-records/app_id={{appId}}',
headers: {
'Authorization': 'Basic {{apiKey}}' // {{apiKey}} explained later
}
},
// and finally, if a string is used, it is to specify just the URL.
// In this case, Appmixer will perform GET request to that URL.
requestProfileInfo: 'https://acme.com/get-profile-info?apiKey={{apiKey}}'
// Note that if you define 'requestAccessToken' property in an Oauth2 authentication
// module with a string that contains a URL, then a POST request will be
// sent to that URL. In other words, what will happen, when a property
// is defined as a string, depends on the context (Oauth vs ApiKey ...).
}
}
module.exports = {
type: 'apiKey',
definition: {
tokenType: 'authentication-token',
auth: {
domain: {
type: 'text',
name: 'Domain',
tooltip: 'Your Freshdesk subdomain - e.g. if the domain is <i>https://example.freshdesk.com</i> just type <b>example</b> inside this field'
},
apiKey: {
type: 'text',
name: 'API Key',
tooltip: 'Log into your Freshdesk account and find <i>Your API Key</i> in Profile settings page.'
}
},
accountNameFromProfileInfo: 'contact.email',
requestProfileInfo: async context => {
// curl https://mydomain.freshdesk.com/api/v2/agents/me \
// -u myApiKey:X'
return context.httpRequest({
method: 'GET',
url: `https://${context.domain}.freshdesk.com/api/v2/agents/me`,
auth: {
user: context.apiKey,
password: 'X'
},
json: true
});
},
validate: async context => {
// curl https://mydomain.freshdesk.com/api/v2/agents/me \
// -u myApiKey:X'
const credentials = `${context.apiKey}:X`;
const encoded = (new Buffer(credentials)).toString('base64');
await context.httpRequest({
method: 'GET',
url: `https://${context.domain}.freshdesk.com/api/v2/agents/me`,
headers: {
'Authorization': `Basic ${encoded}`
}
});
// if the request doesn't fail, return true (exception will be captured in caller)
return true;
}
}
};{
contact: {
email: 'appmixer@example.com',
name: 'Appmixer example',
// More properties here...
}
// There can be more properties here as well...
}'use strict';
module.exports = {
type: 'apiKey',
definition: {
tokenType: 'apiKey',
accountNameFromProfileInfo: 'appId',
auth: {
appId: {
type: 'text',
name: ' APP ID',
tooltip: 'Log into your account and find Api key.'
},
apiKey: {
type: 'text',
name: 'REST API Key',
tooltip: 'Found directly next to your App ID.'
},
cert: {
// just to show, that a 'textarea' input is supported as well
type: 'textarea',
name: 'TLS CA',
tooltip: 'Paste text content of <code>.crt</code> file'
}
},
// In the validate request we need the appId and apiKey specified by the user.
// All properties defined in the previous 'auth' object will be
// available in the validate call. Just use {{whatever-key-from-auth-object}}
// anywhere in the next object. Appmixer will replace these with the
// correct values.
validate: {
method: 'GET',
url: 'https://acme.com/get-some-records/app_id={{appId}}',
headers: {
'Authorization': 'Basic {{apiKey}}'
}
}
}
};
validate: {
'method': 'GET',
'uri': 'https://api.apify.com/v2/users/{{apiUserId}}?token={{apiToken}}'
},
validateErrCallback: err => {
if (err?.someNestedObject?.message) {
throw new Error(err.someNestedObject.message); // message for the UI
}
throw err;
}'use strict';
module.exports = {
type: 'pwd',
definition: {
// As opposed to the 'apiKey' module, the 'auth' property inside the
// definition object is optional for the 'pwd' type. If omitted, it will
// have the following structure. If you want to change name of the field,
// change the content of the tooltip or add another field, you can do
// that here.
auth: {
username: {
type: 'text',
name: 'Username',
tooltip: 'Username'
},
password: {
type: 'password',
name: 'Password',
tooltip: 'Password'
}
},
// the only mandatory property for the 'pwd' authentication type
validate: async context => {
const { username, password } = context
const { data } = await context.httpRequest.post(
// the URL for the username/password authentication
'https://service-server-api-url/auth',
{ username, password }
);
// verify authentication works
if (!data.isValid) {
throw new Error("Invalid username/password combination.");
}
// if the API does not return a token for exchange, then return here.
// in such case, the components using this authentication module
// will have to use the username/password to perform each request.
// There will be properties context.auth.username and
// context.auth.password available in the context object in a component.
// if the API returns a token and expiration then return them using
// the following way - object with 'token' and 'expires' properties.
const { token, expiresIn } = data;
// the token and expiration (if provided) will be available in a
// component's context object under context.auth.token and
// context.auth.expires
return {
token,
// expires can be a Date - token expiration date (timestamp),
// or number/string with seconds representing a token lifetime.
// Appmixer will use this to request a new token (using stored
// username and password) when the current one is about to expire
expires: expiresIn
};
// if the API returns a token that does not expire, just return
// that token
// return { token };
}
}
};
'use strict';
module.exports = {
type: 'pwd',
definition: {
validate: async context => {
await context.httpRequest.get('https://postman-echo.com/basic-auth', {
auth: {
username: context.username,
password: context.password
}
});
}
}
}'use strict';
module.exports = {
async start(context) {
await context.httpRequest.get('https://postman-echo.com/basic-auth', {
auth: {
username: context.auth.username,
password: context.auth.password
}
});
}
}'use strict';
const OAuth = require('oauth').OAuth;
const Promise = require('bluebird');
module.exports = {
type: 'oauth',
// In this example, 'definition' property is defined as a function.
definition: context => {
let trelloOauth = Promise.promisifyAll(new OAuth(
'https://trello.com/1/OAuthGetRequestToken',
'https://trello.com/1/OAuthGetAccessToken',
context.consumerKey,
context.consumerSecret,
'1.0',
context.callbackUrl,
'HMAC-SHA1'
), { multiArgs: true });
return {
accountNameFromProfileInfo: 'id',
authUrl: context => {
return 'https://trello.com/1/OAuthAuthorizeToken' +
'?oauth_token=' + context.requestToken +
'&name=AppMixer' +
'&scope=read,write,account' +
'&expiration=never';
},
requestRequestToken: () => {
return trelloOauth.getOAuthRequestTokenAsync()
.then(result => {
return {
requestToken: result[0],
requestTokenSecret: result[1]
};
});
},
requestAccessToken: context => {
return trelloOauth.getOAuthAccessTokenAsync(
context.requestToken,
context.requestTokenSecret,
context.oauthVerifier
).then(result => {
return {
accessToken: result[0],
accessTokenSecret: result[1]
};
});
},
requestProfileInfo: context => {
return trelloOauth.getProtectedResourceAsync(
'https://api.trello.com/1/members/me',
'GET',
context.accessToken,
context.accessTokenSecret
).then(result => {
if (result[1].statusCode !== 200) {
throw new Error(result[1].statusMessage);
}
result = JSON.parse(result[0]);
// get rid of limits for now
// may and will contain keys with dots - mongo doesn't like it
delete result.limits;
return result;
});
},
validateAccessToken: context => {
return trelloOauth.getProtectedResourceAsync(
'https://api.trello.com/1/tokens/' + context.accessToken,
'GET',
context.accessToken,
context.accessTokenSecret
).then(result => {
if (result[1].statusCode === 401) {
throw new context.InvalidTokenError(result[1].statusMessage);
}
if (result[1].statusCode !== 200) {
throw new Error(result[1].statusMessage);
}
result = JSON.parse(result[0]);
if (result['dateExpires'] === null) {
return;
}
throw new context.InvalidTokenError('Invalid token.');
});
}
};
}
};
'use strict';
const request = require('request-promise');
module.exports = {
type: 'oauth2',
// function definition is used in this case because of the 'profileInfo'
// property that we want to locally store and reference between
// the 'requestAccessToken' and 'requestProfileInfo' functions.
definition: () => {
let profileInfo;
return {
accountNameFromProfileInfo: context => {
return context.profileInfo['email'] || context.profileInfo['id'].toString();
},
authUrl: 'https://app.asana.com/-/oauth_authorize',
requestAccessToken: context => {
// don't put params into post body, won't work, have to be in query
let tokenUrl = 'https://app.asana.com/-/oauth_token?' +
'grant_type=authorization_code&code=' + context.authorizationCode +
'&redirect_uri=' + context.callbackUrl +
'&client_id=' + context.clientId +
'&client_secret=' + context.clientSecret;
return request({
method: 'POST',
url: tokenUrl,
json: true
}).then(result => {
profileInfo = result['data'];
let newDate = new Date();
newDate.setTime(newDate.getTime() + (result['expires_in'] * 1000));
return {
accessToken: result['access_token'],
refreshToken: result['refresh_token'],
accessTokenExpDate: newDate
};
});
},
requestProfileInfo: () => {
return profileInfo;
},
refreshAccessToken: context => {
// don't put params into post body, won't work, have to be in query
let tokenUrl = 'https://app.asana.com/-/oauth_token?' +
'grant_type=refresh_token&refresh_token=' + context.refreshToken +
'&redirect_uri=' + context.callbackUrl +
'&client_id=' + context.clientId +
'&client_secret=' + context.clientSecret;
return request({
method: 'POST',
url: tokenUrl,
json: true
}).then(result => {
profileInfo = result['data'];
let newDate = new Date();
newDate.setTime(newDate.getTime() + (result['expires_in'] * 1000));
return {
accessToken: result['access_token'],
accessTokenExpDate: newDate,
// Some services return a new refresh token, if so, it can
// be returned here and Appmixer will replace the old one.
refreshToken: result['refresh_token']
};
});
},
validateAccessToken: {
method: 'GET',
url: 'https://app.asana.com/api/1.0/users/me',
auth: {
bearer: '{{accessToken}}'
}
}
};
}
};
authUrl: 'https://www.dropbox.com/oauth2/authorize''use strict';
const TENANT = 'common';
module.exports = {
type: 'oauth2',
definition: {
scope: ['offline_access', 'user.read'],
scopeDelimiter: ' ',
authUrl: `https://login.microsoftonline.com/${TENANT}/oauth2/v2.0/authorize`,
requestAccessToken: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
refreshAccessToken: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
accountNameFromProfileInfo: 'displayName',
emailFromProfileInfo: 'mail',
requestProfileInfo: 'https://graph.microsoft.com/v1.0/me',
validateAccessToken: {
method: 'GET',
url: 'https://graph.microsoft.com/v1.0/me',
auth: {
bearer: '{{accessToken}}'
}
}
}
};
module.exports = {
type: 'oauth2',
definition: () => {
// telling Appmixer it cannot refresh it sooner than 30 seconds
// before access token expiration
refreshBeforeExp: 30,
refreshBeforeExpUnits: 'seconds', // 'minutes' by default
// the rest is business as usual
authUrl: 'https://www.acme.com/oaut2/authorize'
...
}
}module.exports = {
type: 'pwd',
definition: {
// the only mandatory property for the 'pwd' authentication type
validate: async context => {
const { username, password } = context;
const { data } = await context.httpRequest.post(
// the URL for the username/password authentication
'https://service-server-api-url/auth',
{ username, password }
);
// verify authentication works
if (!data.isValid) {
throw new Error("Invalid username/password combination.");
}
return true;
}
}
};'use strict';
const GoogleApi = require('googleapis');
const Promise = require('bluebird');
module.exports = {
type: 'oauth2',
definition: initData => {
return {
// The auth.js 'definition' object can have an optional
// 'connectAccountButton' property, which has to contain the 'image'.
connectAccountButton: { image: 'data:image/png;base64,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' },
scope: ['profile', 'email'],
accountNameFromProfileInfo: function(context) {
return context.profileInfo.email;
},
...
}










