Channel apps
This topic describes channels in the Optimizely Connect Platform (OCP), which enable delivering content through any third-party provider from the campaigns feature in the Optimizely Data Platform (ODP) App.
Once installed, channel apps are available to add to any ODP campaign touchpoint.

A channel app must provide a Channel implementation (see App SDK documentation for details). In general, a channel app must respond to the following interface:
- Check if the channel is ready to use. For example, to ensure credentials are configured before attempting to send.
- (Optionally) Dynamically determine the target (customer identifier fields such as email address) for a piece of content. If this information is statically known ahead of time, it should be provided in the
app.yml
file instead. - validate the content settings and template. This may happen at any time and is not strictly associated with the lifecycle of the content itself. Error messages and toasts can be provided to the user when they attempt to launch the campaign.
- publish the content template. Currently, this happens once per campaign run, but in the future, it will only happen when the content changes. Content should be pre-processed if necessary and stored for use during the campaign deliveries.
- (Optionally) prepare for a campaign run, allowing any setup required by a third party to deliver a set of content. If this step is not needed, the method can be left unimplemented and turned off via
app.yml
withchannel.options.prepare
. - deliver content to batches of recipients with substitutions. During a campaign run, the target identifier and substitutions are provided in batches of 100 to be delivered to the user via the channel.
In addition to implementing the channel interface, a channel app must also have:
- The category
channel
specified in theapp.yml
. - The
channel
section of theapp.yml
must also be filled in. forms/content-settings.yml
andforms/content-template.yml
must be provided. These forms are shown to the user when creating content for a campaign to send to this channel. The data from these forms are provided to thevalidate
andpublish
channel methods.
Validation
Channel apps are powered by their forms, which contain user input. Apps have two mechanisms to validate user input before a customer is able to go live with a campaign containing the channel app.
Inline validation
sections:
- key: request
label: Request
elements:
- key: url
type: text
label: URL
help: HTTP URL to deliver to
required: true
validations:
- regex: "^https?:\\/\\/"
message: Must be an http or https url
In this example, the URL MUST start with http://
or https://
. If the user enters anything else, the form will display the error message "Must be an http or https url" before a user can go live. No code is required to enforce validations provided in the form yml. Your app can now assume that when it accesses this form data, the url is a string value that starts with http://
or https://
.
Caution
Be careful about making validation changes with new versions of your app. Updates to your forms MUST be compatible with older campaigns to ensure a smooth user experience. Additionally, form validation added to a newer version may not be enforced on a campaign that was set live with a previous version of your app. You are responsible for migrating the form data at runtime.
Runtime validation
More complicated validation should be done at runtime. Validate is called before a user can go live with their campaign. The app can return errors for each form value that is invalid as well as toasts to display to the user. If the app returns any errors, publish is not called and the campaign cannot go live.
Use this opportunity to create a great user experience!
Example Channel App
An API based channel app might be implemented with the following:
meta:
app_id: api_channel
display_name: API Channel
version: 1.0.16
vendor: optimizely
summary: Combine the power of ODP campaigns with any API to deliver content or updates through other integrations.
support_url: https://apps.optimizely.com/api-channel
contact_email: [email protected]
categories:
- Channel
availability:
- us
runtime: node18
functions:
list_identifiers:
entry_point: ListIdentifiers
description: Lists messaging identifiers for the channel form
channel:
type: api
targeting: dynamic
options:
prepare: false
events:
send: true
delivery: false
delivery_unknown: false
hard_bounce: false
soft_bounce: true
spam_report: false
active_actions: []
sections:
- key: target
label: Targeting
elements:
- key: identifier
label: Target Identifier
type: select
help: Select an ODP Identifier for targeting
required: true
dataSource:
type: app
function: list_identifiers
- key: headers
label: Headers
elements:
- key: h1_key
label: Header Name 1
help: Header name (for example, Content-Type)
type: text
hint: Content-Type
defaultValue: Content-Type
- key: h1_value
label: Header Value 1
help: Header value (for example, application/json)
type: text
hint: application/json
defaultValue: application/json
- key: h2_key
label: Header Name 2
help: Header name (for example, Content-Type)
type: text
hint: Content-Type
- key: h2_value
label: Header Value 2
help: Header value (for example, application/json)
type: text
hint: Content-type
sections:
- key: request
label: Request
elements:
- key: method
label: API Method
help: API Call Method (for example, POST or GET)
type: select
options:
- value: GET
text: GET
- value: POST
text: POST
- value: PUT
text: PUT
- value: PATCH
text: PATCH
- value: DELETE
text: DELETE
hint: Content-Type
defaultValue: Content-Type
- key: url
type: text
label: URL
help: HTTP URL to deliver to
required: true
validations:
- regex: "^https?:\\/\\/"
message: Must be an http or https url
- key: body
type: text
label: Body Content
help: Post Body
multiline: true
defaultValue: "{}"
visible:
operation: any
comparators:
- key: method
equals: 'POST'
- key: method
equals: 'PUT'
- key: method
equals: 'PATCH'
Generated UI in the ODP App Campaign Creator:

Finally, the app must implement the Channel interface to support preview, test sends, and delivery. A simplified example is below:
import * as App from '@zaiusinc/app-sdk';
import {CampaignContent, CampaignDelivery, CampaignTracking, ChannelContentResult, ChannelDeliverOptions, ChannelDeliverResult, ChannelPreviewResult, ChannelPublishOptions, ChannelTargetResult, ChannelValidateOptions, KVHash, storage} from '@zaiusinc/app-sdk';
import {ContentSettingsFormData} from '../lib/ContentSettingsFormData';
import {ContentTemplateFormData} from '../lib/ContentTemplateFormData';
import {previewTemplate} from '../lib/previewTemplate';
/**
* Defines the interface of a channel. The typical channel flow in a campaign run is as follows:
* 1. Check if the channel is `ready` to use
* 2. Dynamically determine the `target` (may be provided statically in `app.yml` instead)
* 3. `publish` the content template (in the future, this will instead happen when the campaign is modified)
* 4. `prepare` for the run (if the method is implemented)
* 5. `deliver` the content to batches of recipients with substitutions
*
* Outside of the campaign run flow, a channel must also be able to `preview` a given piece of content for a batch
* of recipients.
*/
export class Channel extends App.Channel {
/**
* Checks if the channel is ready to use. This should ensure that any required credentials and/or other configuration
* exist and are valid. Reasonable caching should be utilized to prevent excessive requests to external resources.
* @async
* @returns true if the channel is ready to use
*/
public async ready(): Promise<boolean> {
return true;
}
/**
* Dynamically determines campaign targeting requirements. It should also perform any necessary validations on the
* input data. If targeting is always known ahead of time, this should be specified statically via `channel.targeting`
* in `app.yml`. If targeting is based on selections made in the content settings form, this method must be
* implemented and the value in `app.yml` must be set to `dynamic`.
* @async
* @param contentSettings data from the content settings form
* @returns result of the operation
*/
public async target(contentSettings: ContentSettingsFormData): Promise<ChannelTargetResult> {
const result = new ChannelTargetResult();
result.addTargeting({identifier: contentSettings.target.identifier});
return result;
}
/**
* Validates the given content. This should ensure that the content is suitable for use in the current mode, as
* specified in the options (see {@link ChannelValidateOptions.mode}). If specific fields are missing or invalid,
* appropriate error messages should be provided using {@link ChannelContentResult.addError}. Any errors that are
* not linked to a specific field should be provided using {@link ChannelContentResult.addToast}. If no errors of
* either type are returned, the validation is considered successful, and the operation will be allowed to proceed.
* @async
* @param content the content with translated tempaltes
* @param options additional options
* @returns result of the operation
*/
public async validate(
content: CampaignContent, options: ChannelValidateOptions
): Promise<ChannelContentResult> {
const result = new ChannelContentResult();
this.validateSettings(content.settings as ContentSettingsFormData, result);
await this.validateTemplate(content.template as ContentTemplateFormData, result);
return result;
}
/**
* Publishes the given content. This is the place to perform any necessary transformations between the given template
* format and the external system's format. It can be assumed that {@link Channel.validate} has already been called,
* but additional errors may still be detected in this phase and returned in the same way as during validation.
* <p>
* If the content must be stored in an external system, this is also the time to do that. If the content must instead
* be known in `prepare` or `deliver`, it should be placed in the document store for future use.
* <p>
* This method may be called multiple times with the same content key. But for a given content key, it will always
* be called with the same content and options. As such, once successful, it should be treated as an idempotent
* operation at the content key level. So if the given key has already been processed and successfully stored, there
* is no need to process and store it again.
* @async
* @param contentKey unique key for the content
* @param content the content with translated templates
* @param options additional options
* @returns result of the operation
*/
public async publish(
contentKey: string, content: CampaignContent, options: ChannelPublishOptions
): Promise<ChannelContentResult> {
const result = new ChannelContentResult();
if (
this.validateSettings(content.settings as ContentSettingsFormData, result)
&& await this.validateTemplate(content.template as ContentTemplateFormData, result)
) {
await storage.kvStore.put(`content:${contentKey}`, content as CampaignContent & KVHash);
}
return result;
}
/**
* Prepares for a campaign run. This can be used to set up an external entity for use in `deliver` (or perform any
* other processing that should only be performed once per run). If this step is unnecessary, simply do not implement
* the method.
* <p>
* If implemented, this method will be called exactly once per content key involved in a campaign run. If any one of
* these fails, the campaign run will fail.
* @async
* @param contentKey unique key of the content
* @param tracking campaign tracking parameters
* @param options additional options
* @returns result of the operation
*/
// public async prepare?(
// contentKey: string, tracking: CampaignTracking, options: ChannelPrepareOptions
// ): Promise<ChannelPrepareResult>;
/**
* Delivers a batch of messages. This method may be called many times for the same content key, tracking parameters,
* and options, each with a unique batch of recipients and substitutions. It is assumed that a batch either succeeds
* or fails as a whole. There is no partial delivery handling.
* <p>
* If a batch fails, it may be retried (as controlled by the return value). When that happens, the subsequent call(s)
* for that batch will be given the previous result for reference to enable proper recovery logic.
* <p>
* Once a batch succeeds, it will never be given again.
* @async
* @param contentKey unique key of the content
* @param tracking campaign tracking parameters
* @param options additional options
* @param batch of recipients and substitutions
* @param previousResult previous result of the operation, if this is a retry
* @returns result of the operation
*/
public async deliver(
contentKey: string,
tracking: CampaignTracking,
options: ChannelDeliverOptions,
batch: CampaignDelivery[],
previousResult?: ChannelDeliverResult
): Promise<ChannelDeliverResult> {
// TODO: Deliver the batch of content here. See the example API Channel app for details.
}
/**
* Renders a batch of messages into HTML previews. Each preview must be a full HTML page containing a user-friendly
* representation of the message as it would be delivered.
* @async
* @param content the content with translated templates
* @param batch of recipients and substitutions
* @returns result of the operation
*/
public async preview(content: CampaignContent, batch: CampaignDelivery[]): Promise<ChannelPreviewResult> {
const result = new ChannelPreviewResult();
result.setPreviews(batch.map((delivery) => {
const template = content.template as ContentTemplateFormData;
const {method} = template.request;
const [url, body] = this.performSubstitutions(
delivery.substitutions, template.request.url, template.request.body || ''
);
const settings = content.settings as ContentSettingsFormData;
return previewTemplate(this.formatHeaders(settings.headers), `${method} ${url}`, body);
}));
return result;
}
// ...
}
Updated 4 days ago