Code-Native Flows
Just like low-code integrations, code-native integrations can include multiple flows.
Flows consist of a name
and description
, and a stableKey
that uniquely identifies the flow.
It also contains four functions:
onTrigger
is called when a flow is invoked. If a flow is invoked asynchronously, you can return a custom HTTP response to the caller from the trigger.onExecution
is run immediately after theonTrigger
function and is where the bulk of the flow's work is done.onInstanceDeploy
is called when a new instance of the integration is deployed. These functions are optional and can be used to set up resources or perform other tasks when an instance is deployed.onInstanceDelete
is called when an instance of the integration is deleted,
Code-native flow triggers
The onTrigger
function of a flow is called when the flow is invoked.
A simple no-op trigger that is called asynchronously can simply return the payload it was called with:
flow({
// ...
onTrigger: async (context, payload) => {
return Promise.resolve({ payload });
},
});
If you omit the onTrigger
function, the Prismatic platform will automatically use the generic Webhook trigger.
This will yield the payload to the next StereoPannerNode, the onExecution
function.
If you want to return a custom HTTP response to the caller or would like to complete additional work in the trigger, you can additionally return an HttpResponse
object from the trigger.
In this example, suppose the trigger is invoked via an XML webhook payload that looks like this:
<notification>
<type>new_account</type>
<challenge>067DEAB4-B89C-4211-9767-84C96A39CF8C</challenge>
<account>
<first>Nelson</first>
<last>Bighetti</last>
<company>
<name>Hooli</name>
<city>Palo Alto</city>
<state>CA</state>
</company>
</account>
</notification>
The app calling the trigger requires that you parse the XML payload and return the challenge
property in the HTTP response body with an HTTP 200 Response.
You could write a trigger that parses the XML payload and returns the challenge
property:
import { HttpResponse, flow, util } from "@prismatic-io/spectral";
import { XMLParser } from "fast-xml-parser";
flow({
// ...
onTrigger: async (context, payload) => {
// Parse the raw XML from the webhook request
const parser = new XMLParser();
const parsedBody = parser.parse(util.types.toString(payload.rawBody.data));
// Respond to the request with a plaintext response that includes the challenge key
const response: HttpResponse = {
statusCode: 200,
contentType: "text/plain",
body: parsedBody.notification.challenge,
};
// Ensure that the payload is updated with the parsed body
return Promise.resolve({
payload: { ...payload, body: { data: parsedBody } },
response,
});
},
});
Running code-native flows on a schedule
Code-native flows support running on a schedule.
The schedule can either be a static schedule that you define in your code, or you can create a config variable of "schedule"
and let your customer define the schedule.
In the example below, the first flow defines a static schedule of "Run at 10:20 every day on US Central time". For tips on creating cron strings, check out crontab.guru.
The second flow uses a config variable to define the schedule.
import { configPage, flow, integration } from "@prismatic-io/spectral";
const scheduleWithCronExpression = flow({
name: "Schedule Trigger with cron expression",
description: "This flow is triggered by schedule following a cron expression",
stableKey: "schedule-trigger-cron-expression",
onExecution: async (context) => {
const now = new Date();
context.logger.info(`Flow executed at ${now}`);
return Promise.resolve({ data: null });
},
schedule: { cronExpression: "20 10 * * *", timeZone: "America/Chicago" }, // Run at 10:20 AM CST
});
const scheduleWithConfigVar = flow({
name: "Schedule with config var",
description: "This flow is triggered by a schedule following a config var",
stableKey: "schedule-trigger-config-var",
onExecution: async (context) => {
const now = new Date();
context.logger.info(`Flow executed at ${now}`);
return Promise.resolve({ data: null });
},
schedule: { configVarKey: "My Schedule" }, // Run on a user-defined schedule
});
export default integration({
name: "schedule-trigger-test",
description: "Schedule Trigger Test",
iconPath: "icon.png",
flows: [scheduleWithCronExpression, scheduleWithConfigVar],
configPages: {
"Page One": configPage({
elements: {
"My Schedule": {
dataType: "schedule",
stableKey: "my-schedule",
},
},
}),
},
});
Code-native flow onInstanceDeploy and onInstanceDelete
Code-native flows support onInstanceDeploy
and onInstanceDelete
callback functions.
These functions run when an instance of the integration is deployed and deleted respectively.
These functions are handy for setting up resources or performing other tasks when an instance is deployed or deleted, and are often used to set up webhooks in third-party apps.
The functions work the same as custom trigger functions, which are documented in the Writing Custom Components article.
Code-native flow onExecution
The onExecution
function runs immediately after the onTrigger
function, and is where the bulk of the flow's work is done.
The onExecution
function takes two parameters:
context
- in addition to the attributes that a normal custom component receives (like a logger, persisted data, metadata about the integration, customer and instance), a CNI flow'scontext
object also contains aconfigVars
object which has the values of all config variables that your integration includes.params
- theparams
object contains the payload that was returned from theonTrigger
function.
This example onExecution
function performs the same logic that the low-code Build Your First Integration integration did, but in TypeScript:
import { flow } from "@prismatic-io/spectral";
import axios from "axios";
import { createSlackClient } from "../slackClient";
interface TodoItem {
id: number;
completed: boolean;
task: string;
}
export const todoAlertsFlow = flow({
// ...
onExecution: async (context) => {
// Config variables are accessed using the context object
const { logger, configVars } = context;
// Make an HTTP request to the Acme API using the config variable
const { data: todoItems } = await axios.get<TodoItem[]>(
configVars["Acme API Endpoint"],
);
// Create an HTTP Slack client using the Slack OAuth connection
const slackClient = createSlackClient(configVars["Slack OAuth Connection"]);
// Loop over the todo items
for (const item of todoItems) {
if (item.completed) {
logger.info(`Skipping completed item ${item.id}`);
} else {
// Send a message to the Slack channel for each incomplete item
logger.info(`Sending message for item ${item.id}`);
try {
await slackClient.post("chat.postMessage", {
channel: configVars["Select Slack Channel"],
text: `Incomplete task: ${item.task}`,
});
} catch (e) {
throw new Error(`Failed to send message for item ${item.id}: ${e}`);
}
}
}
// Asynchronously-invoked flows should simply return null
return { data: null };
},
});
Referencing the trigger payload in the onExecution function
The trigger will generally return the payload it received, but you can also return a modified payload from the trigger.
The onExecution
function will receive the payload that was returned from the trigger.
The trigger may receive a payload of any format, so annotating a TypeScript interface
is helpful for type hinting and code completion:
import { createSlackClient } from "../slackClient";
interface AccountNotification {
notification: {
type: string;
challenge: string;
account: {
first: string;
last: string;
company: {
name: string;
city: string;
state: string;
};
};
};
}
const sendMessagesFlow = flow({
// ...
onExecution: async (context, params) => {
const { configVars } = context;
const slackClient = createSlackClient(configVars["Slack OAuth Connection"]);
// The parsed XML payload is available in the params object
const data = params.onTrigger.results.body.data as AccountNotification;
// Construct a message to send to Slack
const message =
`New account received:\n` +
`Name: ${data.notification.account.first} ${data.notification.account.last}\n` +
`Company: ${data.notification.account.company.name}\n` +
`Location: ${data.notification.account.company.city}, ${data.notification.account.company.state}\n`;
await slackClient.post("chat.postMessage", {
channel: configVars["Select Slack Channel"],
text: message,
});
return { data: null };
},
});
Flow stable keys
Flows have a user-supplied stableKey
property.
These keys are used to uniquely identify the flow in the Prismatic API, and help guard against inadvertent changes to the name of a flow.
Without a stable key, if a flow name is changed the Prismatic API will treat it as a new flow, and deployed flows will receive new webhook URLs.
With a stable key, the Prismatic API will be able to map the renamed flow and retain its webhook URL.
Stable keys can be any user-supplied string. You can choose a random UUID, or a string that describes the flow or config variable.