Custom Actions
Overview
A component is comprised of zero, one or many actions.
For example, the HTTP component contains actions to GET (httpGet), POST (httpPost), etc.
An action can be added as a step of an integration.
An action has three required properties:
displaywhich affects how the action renders within the Prismatic web application- A series of
inputfields - A function to
performwhen the action is encountered in a flow.
An action may return some data that can be used in a subsequent step.
import { action, input } from "@prismatic-io/spectral";
const myAction = action({
display: {
label: "Say Hello",
description: "Concatenate the first and last name of a person",
},
inputs: {
firstName: input({ label: "First Name", type: "string", required: true }),
lastName: input({ label: "Last Name", type: "string", required: true }),
},
perform: async (context, inputs) => {
const myMessage = `Hello, ${inputs.firstName} ${inputs.lastName}`;
return Promise.resolve({ data: myMessage });
},
});
The perform function
Each action contains one perform function, which is an async function with two parameters that may or may not have a return value.
In this example firstName, middleName, and lastName, are input fields for this perform function:
export const properFormatName = action({
display: {
label: "Properly Format Name",
description: "Properly format a person's name (Last, First M.)",
},
perform: async (context, inputs) => {
if (inputs.middleName) {
return {
data: `${inputs.lastName}, ${inputs.firstName} ${inputs.middleName[0]}.`,
};
} else {
return { data: `${inputs.lastName}, ${inputs.firstName}` };
}
},
inputs: { firstName, middleName, lastName },
});
perform Function Parameters
The perform function takes two positional parameters, context and inputs, that can be destructured into their respective properties:
perform: async (context, inputs) => {},
// or
perform: async (
{ logger },
{ paramName1, paramName2, ... }
) => {},
The context parameter
The context parameter is an object that contains the following attributes:
loggerallows you to write out log lines.debugis an object which you can use when debug mode is enabled to emit additional debug log lines or measure time or memory costs of specific portions of your code.instanceState,crossFlowState,integrationStateandexecutionStategives you access to persisted state.stepIdis the ID of the current step being executed.executionIdis the ID of the current execution.webhookUrlscontains the URLs of the running instance's sibling flows.webhookApiKeyscontains the API keys of the running instance's sibling flows.invokeUrlwas the URL used to invoke the integration.customeris an object containing anid,name, andexternalIdof the customer the instance is assigned to.useris an object containing anid,name,email(their ID) andexternalIdof the customer user whose user-level config was used for this execution. This only applies to instances with User Level Configuration.integrationis an object containing anid,name, andversionSequenceIdof the integration the instance was created from.instanceis an object containing anidandnameof the running instance.flowis an object containing theidandnameof the running flow.invokeFlowis a function that lets you invoke another flow by name. Generally, you'll want to use the Invoke Flow action which wraps this function.
Step ID
context.stepId contains the unique identifier (UUID) of the step.
It is used by the Process Data - DeDuplicate action to track what items in a array have or have not been previously seen.
You can use it similarly in a custom component to persist step-specific data.
Webhook URLs
You can reference an instance's webhook URLs through the context.webhookUrls object.
This is useful when writing actions to configure and delete webhooks in a third-party app.
perform: async (context, inputs) => {
const inventoryUrl = context.webhookUrls["My Inventory Flow"];
};
You can reference context.flow.name to fetch the current flow's webhook URL:
perform: async (context, inputs) => {
const myCurrentUrl = context.webhookUrls[context.flow.name];
};
Logger object
context.logger is a logging object and can be helpful to debug components.
perform: async ({ logger }, inputs) => {
logger.info("Things are going great");
logger.warn("Now less great...");
};
Available log functions in increasing order of severity include logger.debug, logger.info, logger.warn and logger.error.
You can also execute logger.metric on an object, which helps when streaming logs and metrics to an external logging service.
Note: Log lines are truncated after 4096 characters. If you need longer log lines, consider streaming logs to an external log service.
Execution, instance, and cross-flow state
context.executionState, context.instanceState, context.integrationState and context.crossFlowState are key/value stores that may be used to store small amounts of data for future use:
context.executionStatestores state for the duration of the execution, and is often used as an accumulator for loops.context.instanceStatestores state that is persisted between executions. This state is scoped to a specific flow. The flow may persist data, and reference it in a subsequent execution.Shouldn'tinstanceStatebe calledflowState?Great question! We developed state storage prior to multi-flow, and the name
instanceStatewas retained for historical reasons.context.crossFlowStatealso stores state that is persisted between executions. This state is scoped to the instance, and flows may reference one another's stored state.context.integrationStatestores state between flows in instances of the same integration. Customer A's flow 1 can share data with Customer B's flow 2.
State is most notably used by the Persist Data and Process Data components, but you can use it in your custom components, too.
If, for example, a previous flow's run saved a state key of sampleKey, you can reference context.instanceState['sampleKey'] to access that key's value.
To do the reverse, and save data to a flow's state storage for subsequent runs, add an instanceState property to your perform function's return value:
return {
data: "Some Data",
instanceState: { exampleKey: "example value", anotherKey: [1, 2, 3] },
};
Note: To remove a key from persisted state, set it to null:
return {
data: "Some Data",
crossFlowState: { exampleKey: null },
};
Input parameters
The inputs parameter is an object that has attributes for each input field the action supports.
For example, for the perform action defined above, inputs has inputs.firstName, inputs.middleName, and inputs.lastName.
firstName, middleName, and lastName are based off of the input objects that are provided to the action as inputs.
You can use shorthand property names for inputs.
If your input object variables have different names - say you have a const myFirstNameInput = input ({...}), you can structure your action's input property like this:
inputs: {
firstName: myFirstNameInput,
middleName: myMiddleNameInput,
lastName: myLastNameInput,
}
and the inputs object passed into perform will have keys firstName, middleName, and lastName.
Using non-shorthand property names is preferable to some developers to avoid variable shadowing.
The function is written with a destructured inputs parameter.
It could be rewritten without being destructured.
perform: async (context, inputs) => {
if (inputs.middleName == "") {
return { data: `${inputs.lastName}, ${inputs.firstName}` };
} else {
return {
data: `${inputs.lastName}, ${inputs.firstName} ${inputs.middleName[0]}.`,
};
}
},
Coercing input types
TypeScript-based Node libraries often have strict rules about the type of variables that are passed into their functions, but inputs to perform functions are of type unknown since it's not known ahead of time what types of values users of components are going to pass in.
For example, you might expect one of your inputs to be a number, but a user might pass in a string instead.
That's obviously a problem since "2" + 3 is "23", while 2 + 3 is 5 in JavaScript.
The Spectral package includes several utility functions for coercing input to be the type of variable that you need.
Looking at the number/string example, suppose you have some input - quantity - that you need turned into a number (even if someone passes in "123.45" as a string), and you have another input - itemName - that you'd like to be a string.
You can use util.types.toNumber() and util.types.toString() to ensure that the input has been converted to a number and string respectively:
import { action, util } from "@prismatic-io/spectral";
import { someThirdPartyApiCall } from "some-example-third-party-library";
action({
/*...*/
perform: async (context, { quantity, itemName }) => {
const response = await someThirdPartyApiCall({
orderQuantity: util.types.toNumber(quantity), // Guaranteed to be a number
orderItemName: util.types.toString(itemName), // Guaranteed to be a string
});
return { data: response };
},
});
If an input cannot be coerced into the type you've chosen - for example, suppose you pass "Hello World" into util.toNumber() - an error will be thrown indicating that "Hello World" cannot be coerced into a number.
Writing your own type checking functions
Prismatic provides a variety of type check and type coercion functions for common types (number, integer, string, boolean, etc). If you require a uniquely shaped object, you can create your own type check and coercion functions to ensure that inputs your custom component receives have the proper shape that the libraries you rely on expect.
You can import an interface or type (or write one yourself) and write a function that converts inputs into an expected shape.
For example, the SendGrid SDK expects an object that has this form:
{
"to": [string],
"from": string,
"subject": string,
"text": string,
"html": string
}
We can pull in that defined type, MailDataRequired, from the SendGrid SDK, and write a function that takes inputs and converts them to an object containing a series of strings:
import { MailDataRequired } from "@sendgrid/mail";
import { util } from "@prismatic-io/spectral";
export const createEmailPayload = ({
to,
from,
subject,
text,
html,
}): MailDataRequired => ({
to: util.types
.toString(to)
.split(",")
.map((recipient: string) => recipient.trim()),
from: util.types.toString(from),
subject: util.types.toString(subject),
text: util.types.toString(text),
html: util.types.toString(html),
});
Perform function return values
An action's perform function can return a variety of data, outlined in step outputs
Setting synchronous HTTP status codes
If you invoke your instances synchronously and would like to return an HTTP status code other than 200 - OK, you can configure the final step of your integration to be a custom component that returns any HTTP status code you want.
To return an HTTP status code other than 200, return a statusCode attribute in the object you return from your custom component instead of a data attribute:
return {
statusCode: 415,
};
If this custom component is the last step of an integration, then the integration will return an HTTP status code of 415 if invoked synchronously.
Note: When an integration is invoked synchronously, by default the integration redirects the caller to a URL containing the output results of the final step of the integration.
If the final step of the integration is a Stop Execution action, or any custom component action that returns a statusCode, the redirect does not occur.
Instead, the caller receives an HTTP response with the statusCode specified.
Read more about HTTP status codes for synchronous integrations.