Code-Native Config Wizard
Like low-code integrations, code-native integrations include a config wizard. The config wizard can include things like OAuth 2.0 connections, API key connections, dynamically-sourced UI elements (data sources), and other advanced configuration wizard steps.
A config wizard consists of multiple pages.
Each page has a title, which is derived from the key of the configPage object, and a tagline as well as a set of elements (individual config variables).
For example, a config wizard might contain a page for a Slack OAuth 2.0 connection, a page where the user selects a channel from a dynamically-populated dropdown menu, and a page where a user enters two static string inputs:
import { configPage, configVar } from "@prismatic-io/spectral";
import { slackConnectionConfigVar } from "./connections";
import { slackSelectChannelDataSource } from "./dataSources";
export const configPages = {
Connections: configPage({
tagline: "Authenticate with Slack",
elements: {
"Slack OAuth Connection": slackConnectionConfigVar,
},
}),
"Slack Config": configPage({
tagline: "Select a Slack channel from a dropdown menu",
elements: {
"Select Slack Channel": slackSelectChannelDataSource,
},
}),
"Other Config": configPage({
elements: {
"Acme API Endpoint": configVar({
stableKey: "acme-api-endpoint",
dataType: "string",
description: "The endpoint to fetch TODO items from Acme",
defaultValue:
"https://my-json-server.typicode.com/prismatic-io/placeholder-data/todo",
}),
"Webhook Config Endpoint": configVar({
stableKey: "webhook-config-endpoint",
dataType: "string",
description:
"The endpoint to call when deploying or deleting an instance",
}),
},
}),
};
Config variable visibility in code-native
Each config variable can have a permissionAndVisibilityType property with one of three values:
customeris the default value. Customer users can view and edit the config variable, and it will always appear in the config wizard.embeddedmakes it so the config variable does not show up in the config wizard, but your app is able to set it programmatically through the embedded SDK. This is useful if you want to set an API key for a user during the configuration process but not allow the user to see or edit the value that is set.organizationmakes it so the config variable is not visible to your customer and is not able to be set programmatically by your app. Config variables marked organization must have a default value, or else your team members will need to set the value on behalf of your customer.
Additionally, visibleToOrgDeployer determines if an organization user will see this config variable in the config wizard UI.
While organization team members always have programmatic access to instances' config variables and their values, this helps to visually conceal some config variable values like generated metadata from data sources, etc.
Defaults to true.
configVar({
stableKey: "debug",
dataType: "boolean",
description: "Enable debug logging",
defaultValue: "false",
permissionAndVisibilityType: "customer",
visibleToOrgDeployer: true,
});
Connections in code-native integrations
Connection definitions in CNI are very similar to custom component connection definitions, but you use the connectionConfigVar function instead of the connection function.
For example, a Slack OAuth 2.0 connection might look like this:
export const slackConnectionConfigVar = connectionConfigVar({
stableKey: "slack-oauth-connection",
dataType: "connection",
oauth2Type: OAuth2Type.AuthorizationCode,
inputs: {
authorizeUrl: {
label: "Authorize URL",
placeholder: "Authorize URL",
type: "string",
default: "https://slack.com/oauth/v2/authorize",
required: true,
shown: false,
comments: "The OAuth 2.0 Authorization URL for the API",
},
tokenUrl: {
label: "Token URL",
placeholder: "Token URL",
type: "string",
default: "https://slack.com/api/oauth.v2.access",
required: true,
shown: false,
comments: "The OAuth 2.0 Token URL for the API",
},
revokeUrl: {
label: "Revoke URL",
placeholder: "Revoke URL",
type: "string",
required: true,
shown: false,
comments: "The OAuth 2.0 Revocation URL for Slack",
default: "https://slack.com/api/auth.revoke",
},
scopes: {
label: "Scopes",
placeholder: "Scopes",
type: "string",
required: false,
shown: false,
comments: "Space separated OAuth 2.0 permission scopes for the API",
default: "chat:write chat:write.public channels:read",
},
clientId: {
label: "Client ID",
placeholder: "Client ID",
type: "string",
required: true,
shown: false,
comments: "Client Identifier of your app for the API",
default: SLACK_CLIENT_ID,
},
clientSecret: {
label: "Client Secret",
placeholder: "Client Secret",
type: "password",
required: true,
shown: false,
comments: "Client Secret of your app for the API",
default: SLACK_CLIENT_SECRET,
},
signingSecret: {
label: "Signing Secret",
type: "password",
required: true,
shown: false,
default: SLACK_SIGNING_SECRET,
},
},
});
The above OAuth 2.0 connection would be rendered in a config wizard as a card with a connect button that a user can click to authenticate with Slack.

Connection inputs yield objects.
To access one of the fields in the connection definition (like the signingSecret input) in a trigger or onExecution function, you can access it using the configVars object in the context parameter.
// Access a field defined in the connection definition
context.configVars["Slack OAuth Connection"].fields.signingSecret;
// Access the access token from the OAuth 2.0 flow
context.configVars["Slack OAuth Connection"].token?.access_token;
Integration-agnostic connections in code-native
Integration-agnostic connections are centrally managed and can be referenced across one or multiple integrations. You can configure
- org-activated global connections (all customers share a connection)
- org-activated customer connectors (you as an organization create a connection for your customer)
- customer-activated connections (your customer creates their own connection which can be used in multiple integrations).
To use an integration-agnostic connection in a code-native integration, take note of the connection's stable key.
If the connection is an customer-activated connection, use the customerActivatedConnection function and add your connection to the first configPage in your config wizard.
import {
configPage,
customerActivatedConnection,
} from "@prismatic-io/spectral";
export const configPages = {
Connections: configPage({
elements: {
// Customer-activated connection
"Salesforce Connection": customerActivatedConnection({
stableKey: "acme-sfdc-connection",
}),
},
}),
};
If the connection is an org-activated customer or org-activated global connection, use the organizationActivatedConnection function and add the connection to the scopedConfigVars property of your integration definition in index.ts.
Be sure to export scopedConfigVars from your index.ts file so TypeScript can infer config variable types in your flows.
import {
integration,
organizationActivatedConnection,
} from "@prismatic-io/spectral";
import flows from "./flows";
import { configPages } from "./configPages";
import { componentRegistry } from "./componentRegistry";
export { configPages } from "./configPages";
export { componentRegistry } from "./componentRegistry";
export const scopedConfigVars = {
// Org-activated customer connection
"Acme API Key": organizationActivatedConnection({
stableKey: "acme-api-key",
}),
// Org-activated global connection
"OpenAI API Key": organizationActivatedConnection({
stableKey: "openai-api-key",
}),
};
export default integration({
name: "Acme OpenAI Integration",
description: "Connect Acme to OpenAI",
iconPath: "icon.png",
flows,
configPages,
componentRegistry,
scopedConfigVars,
});
Data sources in code-native integrations
Data sources are defined in CNI similar to how they are defined in custom components, but you use the dataSourceConfigVar function rather than the dataSource function.
For example, if you would like to add a picklist dropdown menu to your config wizard that displays a list of Slack channels, you could define a data source like this:
import {
Connection,
Element,
dataSourceConfigVar,
} from "@prismatic-io/spectral";
import { createSlackClient } from "./slackClient";
import { AxiosResponse } from "axios";
interface Channel {
id: string;
name: string;
}
interface ListChannelsResponse {
ok: boolean;
channels: Channel[];
response_metadata?: {
next_cursor: string;
};
}
export const slackSelectChannelDataSource = dataSourceConfigVar({
stableKey: "slack-channel-selection",
dataSourceType: "picklist",
perform: async (context) => {
const client = createSlackClient(
context.configVars["Slack OAuth Connection"] as Connection,
);
let channels: Channel[] = [];
let cursor = null;
let counter = 1;
// Loop over pages of conversations, fetching up to 10,000 channels
// If we loop more than 10 times, we risk hitting Slack API limits,
// and returning over 10,000 channels can cause the UI to hang
do {
const response: AxiosResponse<ListChannelsResponse> = await client.get(
"conversations.list",
{
params: {
exclude_archived: true,
types: "public_channel",
cursor,
limit: 1000,
},
},
);
if (!response.data.ok) {
throw new Error(
`Error when fetching data from Slack: ${response.data}`,
);
}
channels = [...channels, ...response.data.channels];
cursor = response.data.response_metadata?.next_cursor;
counter += 1;
} while (cursor && counter < 10);
// Map conversations to key/label objects, sorted by name
const objects = channels
.sort((a, b) => (a.name < b.name ? -1 : 1))
.map<Element>((channel) => ({
key: channel.id,
label: channel.name,
}));
return { result: objects };
},
});
The above data source would be rendered in a config wizard as a picklist dropdown menu that displays a list of Slack channels.

You can build advanced UI elements, like field mappers, into your config wizard using JSON Forms data sources.
Resetting JSON Forms data sources
When a JSON Forms data source is first configured, it can return default data that pre-fills the form for your users.
If a user reconfigures the instance, their previous selections are retained by default, and the data source's default data is ignored.
However, you may want to reset the data source to its default values when certain config variables change. For example, suppose your config wizard has three pages:
- Page 1: Connect to Microsoft SharePoint with OAuth 2.0
- Page 2: Select a SharePoint site from a dropdown menu
- Page 3: Present a JSON form with default data from the selected site
If the user changes their site selection on page 2, you may want to reset the JSON form on page 3 to show fresh default data for the newly selected site.
You can accomplish this by adding a dataSourceReset property to your JSON Forms data source config variable.
The dataSourceReset object includes two properties:
- mode: Determines how the reset behaves. Options are
"prompt"(asks the user if they want to reset) or"always"(resets automatically) - dependencies: An array of config variable names to watch. When any of these config variables change, the reset logic is triggered
import { dataSourceConfigVar } from "@prismatic-io/spectral";
dataSourceConfigVar({
stableKey: "posts",
dataSourceType: "jsonForm",
dataSourceReset: {
mode: "prompt",
dependencies: ["User"],
},
perform: async (context) => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${context.configVars["User"]}/posts`,
);
const posts = (await response.json()) as Post[];
const schema = {
type: "array",
items: {
type: "string",
oneOf: posts.map((post) => ({
title: post.title,
const: `${post.id}`,
})),
},
};
const uiSchema = {
type: "VerticalLayout",
elements: [
{
type: "Control",
scope: "#",
label: "Posts",
},
],
};
const data = [`${posts[0].id}`, `${posts[1].id}`];
return {
result: {
schema,
uiSchema,
data,
},
};
},
});
In this example, whenever the User config variable changes, your customer will be asked if they want to reset the posts data source to its default values.
Reset modes:
- prompt: The user is notified that their data may be stale and given the option to reset the form to default values
- always: The data source automatically resets to default values whenever a dependency changes
This reset functionality works the same way as detecting changes to inputs and overriding default data in low-code integrations.
Other config variable types in code-native integrations
Other types of config variables that Prismatic supports (picklists, text inputs, schedules, etc) can be added to CNI integrations, as well.
These are generally defined inline alongside other elements in a configPage:
export const configPages = {
// ...
"Other Config": configPage({
"Acme API Endpoint": configVar({
stableKey: "1F886045-27E7-452B-9B44-776863F6A862",
dataType: "string",
description: "The endpoint to fetch TODO items from Acme",
defaultValue:
"https://my-json-server.typicode.com/prismatic-io/placeholder-data/todo",
}),
}),
};

Additional config wizard helper text in code-native integrations
In addition to config variables, you can add helpful text and images to guide your customers as they work through your config wizard.
To add HTML to the config wizard (which can include links, images, etc), include a string element to a configPage definition:
export const configPages = {
Connections: configPage({
elements: {
helpertext1: "<h2>Asana Instructions</h2>",
helpertext2:
"To generate an Asana API Key, visit the " +
'<a href="https://app.asana.com/0/my-apps" target="_blank">developer portal</a> ' +
'and select "Create new token".',
"Asana API Key": connectionConfigVar({
stableKey: "f0eab60f-545b-4b46-bebf-04d3aca6b63c",
dataType: "connection",
inputs: {
// ...
},
}),
},
}),
};

User-level config wizards in code-native integrations
If your integration relies on user-level config, you can add a user-level config wizard similar to how you create the integration's config wizard.
Within configPages.ts create a userLevelConfigPages object that has the same shape as configPages:
export const userLevelConfigPages = {
Options: configPage({
elements: {
"My ULC Config Variables": configVar({
dataType: "string",
stableKey: "my-ulc-config-var",
description: "Enter a widget value",
}),
},
}),
};
Then, in index.ts import the userLevelConfigPages object.
Provide the object as an export of your project (so TypeScript can infer types via .spectral/index.ts), and include it in your integration() definition:
import { integration } from "@prismatic-io/spectral";
import flows from "./flows";
import { configPages, userLevelConfigPages } from "./configPages";
import { componentRegistry } from "./componentRegistry";
export { configPages, userLevelConfigPages } from "./configPages";
export { componentRegistry } from "./componentRegistry";
export default integration({
name: "ulc-example",
description: "My user-level config example integration",
iconPath: "icon.png",
flows,
configPages,
userLevelConfigPages,
componentRegistry,
});
Config variable stable keys
Config variables each have a user-supplied stableKey property.
These keys are used to uniquely identify the config variable in the Prismatic API, and help guard against inadvertent changes to the name of the config variable.
Without a stable key, if a config variable's name can be changed the Prismatic API will treat it as a new config variable and existing values assigned to the config variable will be lost.
With a stable key, the Prismatic API will be able to map the old config variable to the renamed one, and retain config variable values.
Stable keys can be any user-supplied string. You can choose a random UUID, or a string that describes the flow or config variable.