Skip to main content

Step Inputs

Actions, Triggers and Data Sources are configured via inputs. Each input is comprised of a required label and type, and optional placeholder, default, comments, required and model.

Consider this example input:

const middleName = input({
label: "Middle Name",
placeholder: "Middle name of a person",
type: "string",
required: false,
default: "",
comments: "Leave blank if the user has no middle name",
clean: (value) => util.types.toString(value),
});

This contributes to an input prompt that looks like this:

Step Config - Properly Format Name in Prismatic app

Note where the label and placeholder text appeared in the web application, and note that First Name and Last Name are required - indicated with a *, but Middle Name is not.

Input types

An input can take a number of types, which affects how the input renders in the Prismatic web application:

  • string will allow users to input or reference a string of characters. String input in Prismatic app
  • password will allow users to input or reference a string of characters, and the string will be obfuscated in the UI. Password input in Prismatic app
  • boolean allows users to enter one of two values: true or false. Boolean input in Prismatic app
  • code opens a code editor so users can enter XML, HTML, JSON, etc. Syntax highlighting can be added to a code input's definition and can reference any language supported by PrismJS. (e.g. input({ label: "My Code", type: "code", language: "json" })) Code editor in Prismatic app
  • conditional allows users to enter a series of logical conditionals. This is most notably used in the branch component. Conditional input in Prismatic app

You can also create connection inputs for actions. Read more about connections.

Rather than allowing integration builders to enter values for an input, you might want to have users choose a value from a list of possible values. You can do that by making your input into a dropdown menu.

Dropdown menu in Prismatic app

To create an input with a dropdown menu, add a model property to your input:

export const acmeEnvironment = input({
label: "Acme Inc Environment to Use",
placeholder: "ACME Environment",
type: "string",
required: true,
model: [
{
label: "Production",
value: "https://api.acme.com/",
},
{
label: "Staging",
value: "https://staging.acme.com/api",
},
{
label: "Sandbox",
value: "https://sandbox.acme.com/api",
},
],
});

The model property should be an array of objects, with each object containing a label and a value. The label is shown in the dropdown menu. The value is passed in as the input's value to the custom component.

Collection inputs

Most inputs represent single strings. A collection input, on the other hand, represents an array of values or key-value pairs. Collections are handy when you don't know how many items a component user might need.

Value list collection

For example, your component might require an array of record to query, but you might not know how many record IDs a component user will enter. You can create a valuelist collection in code like this:

Value List Collection Example
const assetIdsInputField = input({
label: "Asset ID(s)",
type: "string",
collection: "valuelist",
required: true,
});

The corresponding UI in the integration designer would then prompt a user for any number of record IDs that they would like to enter:

Value List collection in Prismatic app

When the input is received by an action's perform function, the input is a string[].

Key value list collection

If you would like users to enter a number of key-value pairs as an input, you can use a keyvaluelist collection. The Header input on the HTTP component actions is an example of a keyvaluelist collection, and is defined in code like this:

Key Value List Input
export const headersInputField = input({
label: "Header",
type: "string",
collection: "keyvaluelist",
required: false,
comments: "A list of headers to send with the request.",
example: "User-Agent: curl/7.64.1",
});

The "Header" input, then, appears like this in the integration designer:

Key Value List Collection in Prismatic app

When the input is received by an action's perform function, the input is an array of objects of the form:

[
{
key: "foo",
value: "bar",
},
{
key: "baz",
value: 5,
},
];

If you would like to convert the input to a key-value pair object, you can use the built-in Spectral function, keyValPairListToObject:

import { util } from "@prismatic-io/spectral";
const myObject = util.types.keyValPairListToObject(myInput);
// { foo: "bar", baz: 5 }

Structured object inputs

A structured object input groups related sub-inputs into a single named object. Use structuredObjectInput when an action needs to accept a complex type with named fields - such as a mailing address, a person's name, or any nested record. The integration designer presents each sub-field as its own input row under a collapsible group.

Import structuredObjectInput from @prismatic-io/spectral and pass it a map of inputs:

Structured object input
import { action, input, structuredObjectInput } from "@prismatic-io/spectral";

const createContactAction = action({
display: {
label: "Create Contact",
description: "Create a new CRM contact",
},
inputs: {
connection: connectionInput,
name: structuredObjectInput({
label: "Name",
inputs: {
prefix: input({ label: "Prefix", type: "string" }),
firstName: input({
label: "First Name",
type: "string",
required: true,
}),
lastName: input({ label: "Last Name", type: "string", required: true }),
},
}),
address: structuredObjectInput({
label: "Mailing Address",
inputs: {
street: input({ label: "Street", type: "string" }),
city: input({ label: "City", type: "string" }),
state: input({ label: "State", type: "string" }),
zip: input({ label: "Zip Code", type: "string" }),
},
}),
},
perform: async (context, inputs) => {
// Structured inputs are plain objects - access sub-fields with dot notation
const fullName =
`${inputs.name.prefix ?? ""} ${inputs.name.firstName} ${inputs.name.lastName}`.trim();
const { street, city, state, zip } = inputs.address;

const client = createCrmClient(inputs.connection);
return {
data: await client.contacts.create({
name: fullName,
street,
city,
state,
zip,
}),
};
},
});

Structured object input in Prismatic app

Dynamic object inputs

A dynamic object input shows a different set of sub-inputs depending on which configuration the integration builder selects. This is useful when a single action must handle multiple distinct record types - for example, creating an Account, a Lead, or a Contact in a CRM - where each type has its own fields.

Import dynamicObjectInput from @prismatic-io/spectral and define a configurations map where each key names a configuration:

Dynamic object input - CRM record types
import { action, input, dynamicObjectInput } from "@prismatic-io/spectral";

const createRecordAction = action({
display: {
label: "Create Record",
description: "Create an Account, Lead, or Contact in your CRM",
},
inputs: {
connection: connectionInput,
record: dynamicObjectInput({
label: "Record",
configurations: {
account: {
label: "Account",
inputs: {
companyName: input({
label: "Company Name",
type: "string",
required: true,
}),
industry: input({
label: "Industry",
type: "string",
model: [
{ label: "Technology", value: "tech" },
{ label: "Finance", value: "finance" },
{ label: "Healthcare", value: "healthcare" },
],
}),
annualRevenue: input({ label: "Annual Revenue", type: "string" }),
},
},
lead: {
label: "Lead",
inputs: {
firstName: input({
label: "First Name",
type: "string",
required: true,
}),
lastName: input({
label: "Last Name",
type: "string",
required: true,
}),
company: input({ label: "Company", type: "string" }),
leadSource: input({
label: "Lead Source",
type: "string",
model: [
{ label: "Web", value: "web" },
{ label: "Referral", value: "referral" },
{ label: "Event", value: "event" },
],
}),
},
},
contact: {
label: "Contact",
inputs: {
firstName: input({
label: "First Name",
type: "string",
required: true,
}),
lastName: input({
label: "Last Name",
type: "string",
required: true,
}),
email: input({ label: "Email", type: "string", required: true }),
phone: input({ label: "Phone", type: "string" }),
},
},
},
}),
},
perform: async (context, inputs) => {
const client = createCrmClient(inputs.connection);

// inputs.record.configuration holds the key the builder selected.
// inputs.record.values holds the input values for that configuration.
if (inputs.record.configuration === "account") {
return {
data: await client.accounts.create({
companyName: inputs.record.values.companyName,
industry: inputs.record.values.industry,
annualRevenue: inputs.record.values.annualRevenue,
}),
};
}

if (inputs.record.configuration === "lead") {
return {
data: await client.leads.create({
firstName: inputs.record.values.firstName,
lastName: inputs.record.values.lastName,
company: inputs.record.values.company,
leadSource: inputs.record.values.leadSource,
}),
};
}

if (inputs.record.configuration === "contact") {
return {
data: await client.contacts.create({
firstName: inputs.record.values.firstName,
lastName: inputs.record.values.lastName,
email: inputs.record.values.email,
phone: inputs.record.values.phone,
}),
};
}
},
});

Note that discriminated unions help in identifying the shape of inputs.record.values based on the value of inputs.record.configuration.

The integration designer shows a dropdown of configuration labels - Account, Lead, or Contact - and displays only the sub-inputs for the selected type. In the perform function, inputs.record.configuration holds the selected configuration key, and inputs.record.values holds the input values the builder filled in for that configuration.

Dynamic object input in Prismatic app

Cleaning inputs

An input of an action can be anything - a number, string, boolean, JavaScript Buffer, a complex object with lots of properties, etc. If you reuse an input for multiple actions, it's handy to do some preprocessing and type conversion on the input. That's where a clean function on an input comes in.

For example, suppose you expect an input to be a number. But, inputs by default are presented to perform functions as strings. You can leverage the util.types.toNumber() utility function and clean property to ensure that the input is presented to the perform function as a number:

Ensure input is a number
const serverPortInput = input({
label: "Server Port",
placeholder: "The port of the API server",
comments: "Look for the number after the colon (my-server.com:3000)"
type: "string",
default: "3000",
required: true,
clean: (value) => util.types.toNumber(value),
});

You can also add validation to the input. For example, if you want to validate that the input is an IPv4 IP address, you can build a more complex clean function:

Validate that an input is an IP address
const validateIpAddress = (value: unknown) => {
const ipAddressRegex =
/^(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))$/;
const inputValue = util.types.toString(value);
if (!ipAddressRegex.test(inputValue)) {
throw new Error(`The value "${inputValue}" is not a valid IP address`);
}
return inputValue;
};

const ipAddressInput = input({
label: "IP Address",
placeholder: "Server IP Address",
type: "string",
default: "192.168.1.1",
required: true,
clean: validateIpAddress,
});

Handle complex inputs in a custom action

When an API endpoint that you're wrapping in a custom action expects a simple payload, like

POST /widgets

{
"name": "string",
"color": "string",
"quantity": "number"
}

it's easy to map each value in the POST request to an input. Here, we'd create a "name" input, a "color" input, and a "quantity" input. Then, we'd apply a clean: util.types.toNumber clean function to the "quantity" input to ensure it is cast to a number.

But, some endpoints expect complex payloads that may contain arrays of objects with optional properties, etc.

POST /widgets
{
"externalId": "abc-123",
"variants": [
{
"name": "Variant 1",
"color": "red",
"price": {
"usd": 5,
"ca": 5.5
}
},
{
"name": "Variant 2",
"color": "blue",
"price": {
"usd": 6
}
}
]
}

In this case, it's likely that an integration builder will want to construct a property like variants programmatically, and it's probably best to present two inputs, "External ID" which is type: "string" and "Variants" which is type: "code". To accommodate both JSON and JavaScript object inputs, use the util.types.toObject function to ensure that what is entered becomes a JavaScript object. For example,

Convert a complex input to an object
const createWidgets = action({
display: {
label: "Create Widgets",
description: "Create widgets and their variants",
},
inputs: {
connection: connectionInput,
externalId: input({
label: "External ID",
type: "string",
comments: "The ID stored in Acme for this Widget type",
clean: util.types.toString,
}),
variants: input({
label: "Variants",
comments:
"Variant types of the widget. Ensure you provide an array of variant objects.",
type: "code",
language: "json",
clean: util.types.toObject,
example: JSON.stringify(
[
{
name: "Variant 1",
color: "red",
price: {
usd: 5,
ca: 5.5,
},
},
{
name: "Variant 2",
color: "blue",
price: {
usd: 6,
},
},
],
null,
2,
),
}),
},
perform: async (context, inputs) => {
const client = createAcmeClient(inputs.connection);
const { data } = await client.post("/widgets", {
externalId: inputs.externalId,
variants: inputs.variants,
});
return { data };
},
});