Behaviour

Components receive incoming messages, process them and generate outgoing messages. The way messages are processed is called the component behaviour. It defines what components do internally and how they react on inputs.

Components are implemented as NodeJS modules that return an object with a set of methods (Component Virtual Methods) that the Appmixer engine understands. Let's start with a simple example, a SendSMS component that has one input port (message), no output ports and its purpose is to send an SMS using the Twilio API.

const twilio = require('twilio');
module.exports = {
receive(context) {
let { fromNumber } = context.properties;
let { accountSID, authenticationToken } = context.auth;
let message = context.messages.message.content;
let client = new twilio(accountSID, authenticationToken);
return client.message.create({
body: message.body,
to: message.to,
from: fromNumber
});
}
};

Component Virtual Methods

As was mentioned in the previous paragraph, components are simple NodeJS modules that can implement a certain set of methods the Appmixer engine understands. The one most important method is the receive() method. This method is called by the engine every time messages are available on the input ports and the component is ready to fire (fire patterns match). The method must return a promise that when resolved, acknowledges the processing of the input messages. If the promise is rejected, the Appmixer engine automatically re-tries to send the messages again in the next turn (with some delay).

Messages that have been rejected 30-times are put in a special internal "dead-letter" queue and never returned to the flow for processing again. They can only be recovered manually by the Administrator.

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

Virtual Method

Description

receive(context)

Called whenever there are new messages on the input ports that the component is ready to consume. This method must return a promise that when resolved, tells the engine that the messages were successfully processed. When rejected, the engine re-tries to send the messages to the component again in the next turn (or with an arbitrary delay).

tick(context)

Called whenever the polling timer sends a tick. This method is usually used by trigger Components.

start(context)

Called when the engine signals the component to start (when the flow starts). This method is usually used by some trigger components that might schedule an internal timer to generate outgoing messages in regular intervals.

stop(context)

Called when the engine signals the component to stop (when the flow stops). This is the right place to do a graceful shutdown if necessary.

pause(context)

It might happen that your flow is paused by the engine (but not stopped). This usually happens when e.g. the network is unavailable to validate access tokens, etc. In this case, the engine gives the component a chance to react.

unpause(context)

Called when the component switches from pause state to running state.

Context

All virtual methods have one argument, the context. The context contains all the information you need to process your messages and send new messages to the output ports.

context.messages

(applies to receive())

Incoming messages. An object with keys pointing to the input ports. Each message has a content property that contains the actual data of the message after all variables have been replaced. For example:

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

Remember, if before running the flow, the input port message was defined in the Inspector using variables:

Variables

where the flow descriptor would contain something like this:

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

the context.messages object contains the result of replacing variables with actual data that was sent through the output port of the connected component, i.e.

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

context.auth

The authentication object. It contains all the tokens you need to call your APIs. The authentication object contains properties that you defined in the auth object in your Authentication module (auth.js) for your service. For example, if our authentication module for our service (auth.js) looks like this:

const twilio = require('twilio');
module.exports = {
type: 'apiKey',
definition() {
return {
tokenType: 'authentication-token',
accountNameFromProfileInfo: 'accountSID',
auth: {
accountSID: {
type: 'text',
name: 'Account SID',
tooltip: 'Log into your Twilio account and find <i>API Credentials</i> on your settings page.'
},
authenticationToken: {
type: 'text',
name: 'Authentication Token',
tooltip: 'Found directly next to your Account SID.'
}
},
validate: context => {
let client = new twilio(context.accountSID, context.authenticationToken);
return client.api.accounts.list();
}
};
}
};

we can use the context.auth.accountSID and context.auth.authenticationToken in the component virtual methods:

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

context.properties

The configuration properties of the component. This corresponds to the properties object from the component manifest file. For example, if our component defines the following properties in the manifest file:

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

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

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

context.state

A persistent state of the component. Sometimes you need to store some date for the component that must be available across multiple receive() calls for the same component instance or when the flow is stopped and restarted again. context.state is a simple object with keys mapped to values that is persistently stored in DB. This object is loaded on demand in each receive() call. It is not recommended to store large amounts of data here. Example:

{
receive(context) {
// Emit a message only once per day for this component instance.
const day = (new Date).getDay();
if (!context.state[day]) {
context.state[day] = true;
context.sendJson({ tick: true }, 'out');
}
}
}

The context.state is especially useful for trigger-type of components when polling an API for changes to store the ID of the latest processed item from the API.

context.sendJson(object, outPort)

A method on the context object that you should call when you want to emit a message on one of the components output ports. The first argument can be any JSON object and the second argument is the name of an output port.

context.saveFile(name, mimeType, buffer)

Save a file to the Appmixer file storage. This function returns a promise that when resolved gives you an ID of the stored file. You can pass this ID through your flow (send it to an output port of your component) so that later components that deal with files can load the file from the Appmixer storage using the file ID.

{
receive(context) {
// getAttachment() is some 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) => {
context.sendJson({ fileId: result.fileId }, 'file')
});
}
}

context.loadFile(fileId)

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

{
receive(context) {
return context.loadFile(context.messages.file.content.fileId)
.then((fileContent) => {
// uploadFileToAPI() is some function that uploads a file to an API
return uploadFileToAPI(context.auth, content.messages.file.content.fileName, fileContent);
});
}
}

context.getWebhookUrl()

Get a URL that you can send data to with HTTP POST. When the webhook URL is called (HTTP POST), the receive() method of your component is called by the engine with context.messages.webhook object set and context.messages.webhook.context.data contains the actual data POSTed to the webhook URL:

module.exports = {
receive(context) {
if (context.messages.webhook) {
// Webhook URL received data.
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").
context.response('<myresponse></myresponse>', 200, { 'Content-Type': 'text/xml' });
return;
}
// Otherwise, normal input port received data.
const input = context.messages.myInPort.content;
// The webhook URL. Do something with it (send to your API, send to other connected,
// components, send to your backend, ...)
const url = context.getWebhookUrl();
}
};

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

context.response(body, statusCode, headers)

Send response to the webhook HTTP call. When you set your component to be a webhook-type of component (webhook: true in your component.json file), context.getWebhookURL() becomes available to you inside your component virtual methods. You can use this URL to send HTTP POST requests to. When a request is received by the component, the context.messages.webhook.content.data contains the body of your HTTP POST call. In order to send a response to this HTTP call, you can use the context.response() method. See context.getWebhookUrl() for details and example.

context.loadVariables()

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

[
{
"variables": {
"dynamic": [
{
"label": "Column A",
"value": "columnA",
"componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
"port": "out"
},
{
"label": "Column B",
"value": "columnB",
"componentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
"port": "out"
}
],
"static": {}
},
"sourceComponentId": "2c724dff-a04b-43ed-9787-c9a891a721cc",
"outPort": "out",
"inPort": "in"
},
{
"variables": {
"dynamic": [
{
"label": "Column C",
"value": "columnC",
"componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
"port": "out"
},
{
"label": "Column D",
"value": "columnD",
"componentId": "71eb1f40-ce29-4963-8922-102739bafee4",
"port": "out"
}
],
"static": {}
},
"sourceComponentId": "71eb1f40-ce29-4963-8922-102739bafee4",
"outPort": "out",
"inPort": "in"
}
]

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

Example:

{
receive(context) {
return context.loadVariables()
.then(data => {
const newSchema = data.reduce((acc, item) => {
return acc.concat(item.variables.dynamic.map(o => ({ label: o.label, value: o. value })));
}, []);
context.sendJson(newSchema, 'leftJoin');
context.sendJson(newSchema, 'innerJoin');
context.sendJson(newSchema, 'rightJoin');
});
}
}