HomeGuidesAPI ReferenceGraphQL
Submit Documentation FeedbackJoin Developer CommunityOptimizely GitHubOptimizely NuGetLog In

Channel

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.

An SMS channel powered by the Zaius Integration PlatformAn SMS channel powered by the Zaius Integration Platform

An SMS channel powered by the Zaius Integration Platform

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:

  1. Check if the channel is ready to use. For example, to ensure credentials are configured before attempting to send.
  2. (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.
  3. 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.
  4. 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.
  5. (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 with channel.options.prepare.
  6. 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:

  1. The category channel specified in the app.yml.
  2. The channel section of the app.yml must also be filled in.
  3. forms/content-settings.yml and forms/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 the validate and publish 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: 0.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
runtime: node12
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:

UI generated by the example aboveUI generated by the example above

UI generated by the example above

Finally, the app must implement the Channel interface to support preview, test sends, and delivery. A simplified example is below:

import * as App from '@zaius/app-sdk';
import {CampaignContent, CampaignDelivery, CampaignTracking, ChannelContentResult, ChannelDeliverOptions, ChannelDeliverResult, ChannelPreviewResult, ChannelPublishOptions, ChannelTargetResult, ChannelValidateOptions, KVHash, storage} from '@zaius/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;
  }

  // ...

}

Did this page help you?