Tutorial: Create a Calendly app

Learn how to create a Calendly app that allows agents to view meeting details in a customer's timeline and in an Insight Card.

This introductory tutorial is intended to guide a new app developer through the process of developing an app within Kustomer that leverages many of the major components currently supported by our Apps Platform.

🚧

Important

Always use a sandbox account or test organization to complete tutorials. You won't be able to uninstall an app once you register the app to an organization.

If you need to request a sandbox account or a test organization for development, see Getting access. If you register an app to a Kustomer organization by accident, you can contact [email protected] to remove the app from view.

This introductory tutorial shows you how to build an app that combines iFrame-based Klass Views (or KViews), Commands, and App Settings to connect with an external API and create a powerful UI experience in Kustomer.

For this tutorial, we'll use the Calendly API along with Workflows and Klasses to build a private app that will configure a Klass View that allows the agent to create meetings in Calendly, and have those meetings populate to the Kustomer timeline for the associated customer.

Calendly lets you save time scheduling meetings by making it easier to decide on a convenient time. With this app integration, you can connect your Calendly account to Kustomer so that you can view meeting details in a customer's timeline and in an Insight Card.

Tutorial goals

By the end of this tutorial, you'll know how to:

  • Build a basic app with a hook and trigger
  • Create a UI using Klass Views
  • Set up Internationalization to serve users in different locales
  • Use Workflows and Workflow Actions to automate and act on calls to an external API
  • Make an App Settings page to allow users to configure the app

Prerequisites

Before you get started, make sure you have the following:

Access to Kustomer and apps platform resources

  • Access to a sandbox account or test organization with at least Administrator access
  • A valid API Key that includes the following roles at the minimum: org.admin.apps and org.permission.apps
  • The base URL for your API with your Kustomer organization name
    • Example: https://organization-name.api.kustomerapp.com
  • (Recommended) Familiarity with the Kustomer App Starter Config repository available on GitHub to generate and register a JSON app definition from JavaScript files

Access to external services

  • A valid Calendly subscription with access to the Calendly API

Step 1: Create the basic app definition

In this step, we'll set up the foundation of the app listing as a JSON file. We want to give the app a title, version, description, and some other details.

You can learn more about the basic JSON app definition properties in App definition basics.

For the purposes of this tutorial we'll start with the following JSON block, which will act as the scaffolding for this new app:

const tutorialApp = 'calendly_tutorial';


export default {
 app: tutorialApp,
 title: 'Calendly App Tutorial',
 version: '0.0.1',
 dependencies: ['kustomer-^1.5.0'],
 description: 'This is the description for **Calendly App Tutorial**',
 appDetails: {
   appDeveloper: {
     name: 'Kustomer',
     website: 'https://kustomer.com',
     supportEmail: '[email protected]',
   },
   externalPlatform: {
     name: 'Calendly',
     website: 'https://calendly.com',
   },
 },
};

At this point, the app has no real functionality but this JSON will serve as our foundation. The properties will be visible on the app directory listing within Kustomer.

📘

The Kustomer app dependency

In the JSON block, you'll see the Kustomer app listed in the dependencies array. That's because we will be creating a workflow as part of this app near the end of the process, and that workflow will depend on the Kustomer app.

With the app scaffolding in place, we can now begin adding the other core components of the app.

Step 2: Add the inbound hook

In this step, we’re going to add the inbound webhook, which will be used to receive data from Calendly within Kustomer. To add hooks, simply add a hooks array to the app definition containing the definitions of each hook you want to add. In our case, we are just adding one hook.

We need to give the hook an eventName. The name must begin with kustomer.app and we suggest that you end it with the app name, followed by a term that helps you identify the event. In this case, we will name it kustomer.app.calendly_tutorial.update-event.

Additionally, we need to set the displayEventName and description, which will appear in Kustomer’s UI when viewing the hook, such as in Settings > Platform > Inbound Webhooks.

Finally, we need to set the hook type. In this case, we will set the type to form, which does not require authentication for the external platform to send a successful request to it.

hooks: [
   {
     eventName: `kustomer.app.${tutorialApp}.update-event`,
     displayEventName: 'Calendly Event Update Hook',
     type: 'form',
     description: 'Sync Calendly Event Updates'
   }
 ],

When Calendly sends a webhook to the URL created by this hook, Kustomer will create a kustomer.app.calendly_tutorial.update-event. These events are often used in combination with triggers, which can be used to initiate a workflow.

📘

What type of hook should I use?

Kustomer supports three types of hooks:

  • Webhooks require authentication, and are best suited for server-to-server communication where verifying the authenticity of the sender is important.
  • Form Hooks do not require any authentication, and are best suited for use in public HTML forms or anywhere authenticity of the sender is not important.
  • Email Hooks are commonly used to update order and shipment details in Kustomer by adding the email hook address in the Bcc field of emails that are already being sent to customers.

For the purposes of this test app, we're using a form hook, but you may wish to use a webhook that requires authentication if it better fits the security needs of your organization.

Learn more in Inbound Webhooks.

Step 3: Add the trigger

Next, you'll set up the trigger. A trigger wraps a hook and serves as the entry point for workflows. When a request is received at an inbound webhook, an event will be created, which can be used as a trigger to kick off a workflow.

To add a trigger, we add a triggers array with one item containing the trigger definition. We then give it an eventName that corresponds to the same event created by the hook we added in the previous step as well as a short description.

triggers: [
   {
     eventName: `kustomer.app.${tutorialApp}.update-event`,
     description: 'A Calendly event update',
   }
 ],

Learn more in Triggers.

Step 4: Define app settings

Apps allow you to add settings which the user can configure and the app can reference from other components, such as in workflows and workflow actions.

For Calendly, we will need the user to add their API token so our app can communicate with the Calendly API on the user’s behalf. To enable this behavior, we will add a setting with the default category which will include the authToken setting. We will make this setting required and give it a type of secret so it will remain secure. This will also prevent the value from being rendered in the UI after saving. We must also define a defaultValue, which in this case will simply be an empty string.

The Kustomer platform will automatically generate an app settings page for the UI based on the settings in the app definition. For example, the settings definition below will result in a simple settings page with one authToken text input field. Since its type is secret, when the user enters a value, the value will be obfuscated with bullet characters (⚫ ).

We can further customize the settings page using the i18n object on the app definition.

settings: {
   default: {
     authToken: {
       type: 'secret',
       defaultValue: '',
       required: true
     }
   }
 }

Learn more in App Settings.

Step 5: Add a workflow action

Workflow actions are reusable elements that can be used within any workflow to interact with Kustomer and with external services. Most commonly, they serve as wrappers for API requests. They also come with some conveniences for working with app settings.

In our Calendly app, we will add a workflow action that performs GET requests against the Calendly API using the “authToken” setting we previously added to the app. To do so, we will add the following properties to the workflow action definition:

  • A name prefixed with kustomer.app, followed by the name of our app and a useful identifier.
  • The type of our action, in this case rest_api.
  • The appSettings needed by the action.
  • An inputTemplate containing the request uri, method, and headers. Our uri will make use of handlebars to make it configurable so we can specify the Calendly uri we want to consume when using our action in a workflow. Our method will be GET and headers will include the Content-Type and authorization, which will reference our authToken setting.
  • The inputSchema will include the uri property we reference in the inputTemplate.
  • The outputTemplate will simply map the response and body.
  • The outputSchema will provide a short definition of the properties in the outputTemplate.
actions: [
   {
     name: `kustomer.app.${tutorialApp}.get-request`,
     type: 'rest_api',
     appSettings: {
       authToken: {
         key: `${tutorialApp}.default.authToken`
       }
     },
     inputTemplate: {
       uri: '{{{uri}}}',
       method: 'GET',
       headers: {
         authorization: 'Bearer {{{authToken}}}',
         'Content-Type': 'application/json',
         Accept: 'application/json'
       },
       json: true
     },
     inputSchema: {
       type: 'object',
       properties: {
         uri: {
           type: 'string'
         }
       },
       required: [
         'uri'
       ],
       additionalProperties: false
     },
     outputTemplate: {
       response: '/#response',
       body: '/#body'
     },
     outputSchema: {
       type: 'object',
       properties: {
         response: {
           type: 'object'
         },
         body: {
           type: 'object'
         }
       },     
       additionalProperties: false
     }
   }
 ]

Learn more in Workflow Actions.

Step 6: Add a klass

A klass defines a schema for a custom data object, often referred to as a custom object or KObject in the Kustomer platform. An app can create custom Klasses to represent external domain objects. In the case of Calendly, we will add a Klass to represent a Calendly Event.

We will give the klass the name calendly-event with an icon calendar and color #3e9cf0, which will be shown when a user views the klass in the Kustomer UI. Note that the icon comes from the Material Design Icon library.

Next we will add the klass properties. Each property name will have a suffix representing the type of the property, such as Str for string, Num for number, and At for date. The property will also be given a displayName, which will function as the property label in the Kustomer UI.

klasses: [
   {
     name: `${tutorialApp}-event`,
     icon: 'calendar',
     color: '#3e9cf0',
     metadata: {
       properties: {
         canceledStr: {
           displayName: 'Canceled'
         },
         cancelReasonStr: {
           displayName: 'Cancel Reason'
         },
         canceledDateStr: {
           displayName: 'Canceled Date'
         },
         eventDescriptionStr: {
           displayName: 'Event Description'
         },
         eventDurationNum: {
           displayName: 'Event Duration'
         },
         eventLocationStr: {
           displayName: 'Event Location'
         },
         eventNameStr: {
           displayName: 'Event Name'
         },
         eventTypeStr: {
           displayName: 'Event Type'
         },
         startTimeAt: {
           displayName: 'Start Time'
         },
         endTimeAt: {
           displayName: 'End Time'
         },
       }
     }
   }
 ]

Learn more in Klasses.

Step 7: Add internationalization

Internationalization allows us to customize app text for different locales. Additionally, it allows us to customize the settings page title, description and field display names / descriptions.

To add internationalization to our app, we can add an i18n property containing an object for each locale we want to support. In our case, we will add a property “en_us” containing the settings page title, description, and the auth token setting display name and description.

i18n: {
   en_us: {
     [`${tutorialApp}.settings.page.title`]: 'Calendly',
     [`${tutorialApp}.settings.page.description`]: 'Configure settings for your Calendly integration with Kustomer. Fill out these fields after generating an access token on the Calendly site. [Learn more](https://help.kustomer.com/en_us/integrate-with-calendly-SkjppgFpv)',
     [`${tutorialApp}.settings.path.default.authToken.displayName`]: 'Calendly API Token',
     [`${tutorialApp}.settings.path.default.authToken.description`]: "Locate this on [Calendly's API & Webhooks](https://calendly.com/integrations/api_webhooks) page."
   }
 },

Learn more in Internationalization.

Step 8: Register and install the app

Before proceeding with the next steps in building out the app, we need to register and install the app in a Kustomer org. That's because we'll want to work with the editors within the Kustomer interface to set up the KView and workflow — but most of the components we need to work with won't exist until the app is installed.

Create the app

You can use the Kustomer App Starter Config repository available on GitHub to generate and register a JSON app definition from JavaScript files.

To use the repository, you'll need to:

  1. Select the Use this template option in GitHub to create a new repository. The new repository will start with the same files and folders as the kustomer-app-starter-config repo.

    You can then clone the new repository to your local machine.

  2. Update the .env.example file with the API base URL and API key for your organization. Rename and save the file as .env. The npm command in this repo pulls these environment variables to register and update apps.

API_BASE_URL=https://<organization-name>.api.kustomerapp.com
API_TOKEN=<API key with at least org.admin.apps and org.permission.apps roles>
  1. Add a new app subfolder to the src directory and add to the new folder an index.js file with the code sample below. For this tutorial example, we will use the subfolder name calendly-tutorial-app.

Replace the following:

  • Replace <organization> with the name of the test organization or sandbox account you're using to test the app.
  • For your site URL, use the URL of the web project page you created in Step 1.

📘

Note

The app definition in the code sample defines a single Klass View, or KView. This KView appears for the Conversation resource as a smartbar-card and renders a DynamicCard that points to your web project page with the current Conversation object passed in as the context: context={context.conversation}.

  1. Since we're creating a new app definition, configure your root src/index.js file to import and include your app definition in the apps array. The app starter config repo uses index.js files with commands to bundle together files into a complete app JSON definition.
import tutorialApp from './calendly_tutorial_<organization>';

const apps = [
    tutorialApp
];

export { apps as default };
import myFirstApp from './my-first-app';
import myFirstAdvancedKviewApp from './my-first-advanced-kview-app';
import myFirstWidgetApp from './my-first-widget-app';
import myFirstOutboundWebhookApp from './my-first-outbound-webhook-app';
import tutorialApp from './calendly_tutorial_<organization>';

const apps = [
    myFirstApp,
    myFirstWidgetApp,
    myFirstAdvancedKviewApp,
    myFirstOutboundWebhookApp,
    tutorialApp
];

export { apps as default };

To learn more, see Use the Kustomer App Starter Config.

Register the app

After you create a new app, run the npm run register-new-version command from your repository folder to register the app to your test organization or sandbox account.

Replace <organization> with the name of your Kustomer organization.

To learn more, see Register or update an app with commands.

If the npm command runs successfully, you will see a response and JSON body similar to the example below returned in the terminal.

The returned JSON body shows app data including the app version, the app id, app title, a unique app identifier, the app name, and more.

📘

Namespacing for private apps

You'll notice a string of characters after your app name (for example, calendly_tutorial_<organization>). The Kustomer Apps Platform automatically namespaces private app names with your orgId to ensure global uniqueness for the app name property.

Check your progress

The new tutorial app should now be available in the App Directory for your Kustomer organization.

To find newly registered apps, go to Settings > Apps > App Directory, and select the Directory tab. If you don't see the Calendly App Tutorial app, try refreshing your browser to load any new apps.

Install your tutorial app

Select and install your private Calendly Tutorial App. When you install the app, the app configures the klass view as a context card for Conversation objects in the timeline.

With the tutorial app installed, we now have the requisite pieces in place to take full advantage of the editors available within Kustomer. Let's return to building out the app itself.

Step 9: Add a Custom Object View (KView)

Custom Object Views, also known as KViews or Klass Views, display data for standard objects or custom objects (KObjects) on the agent timeline in the Kustomer UI. Klass views are rendered based on a JSX template.

Learn more at Klass Views.

In our Calendly app we will add one KView which will display the KObject that gets created by our workflow.

The KView definition will consist of the following:

  • A resource, which references the kobject of the type we created earlier in the klass.
  • A template, which contains JSX specifying how to render the KView.
  • The context, which specifies where we want to place the KView. In our case, we will render it as a separate item directly in the agent’s timeline.
  • A meta object, which gives the KView a displayName, icon, and state.
  • A name, prefixed with kustomer.app. followed by the app name and a useful identifier.

📘

The KView Editor

Kustomer contains a KView editor in the UI. For the purposes of adding a KView to an app, sometimes it is helpful to first create the app definition with your klass that you want to display in the KView, then install the app and use Kustomer’s KView editor to write the KView JSX template.

In this case, you’ll be able to find the KView editor by navigating to:

Kustomer > Settings > Platform > Klasses > Calendly event > Timeline layout > Edit

Before saving the template, open Chrome’s dev tools and click the Network tab. After saving the template, you can find and select the PUT request to v1/web-api/kviews/kobject.calendly_tutorial-event in the network tab in Chrome’s dev tools. In the Headers tab for this request, you can find the Request Payload at the bottom which will contain the stringified template. You can then copy this value and use it as your template value in the kview definition.1.

Our KView will look like the following:

kviews: [
   {
     resource: `kobject.${tutorialApp}-event`,
     template: "const data = this;\n\n<div>\n  {_.get(data, 'kobject.custom') ?\n<Segment>\n  <Grid>\n    <Column size=\"eight\">\n      <h4>Invitation Information</h4>\n      <Field\n        field=\"eventTypeStr\"\n        key=\"eventTypeStr\"\n        type=\"kobject.calendly_tutorial-event\"\n        value={object.custom.eventTypeStr}\n      />\n      <Field\n        field=\"eventNameStr\"\n        key=\"eventNameStr\"\n        type=\"kobject.calendly_tutorial-event\"\n        value={object.custom.eventNameStr}\n      />\n      <Field\n        field=\"eventDescriptionStr\"\n        key=\"eventDescriptionStr\"\n        type=\"kobject.calendly_tutorial-event\"\n        value={object.custom.eventDescriptionStr}\n      />\n      <BasicField\n        label=\"Event Duration\"\n        value={`${_.get(data, 'kobject.custom.eventDurationNum').toString()} minutes`}\n      />\n      <Field\n        field=\"startTimeAt\"\n        key=\"startTimeAt\"\n        type=\"kobject.calendly_tutorial-event\"\n        value={object.custom.startTimeAt}\n      />\n      <Field\n        field=\"endTimeAt\"\n        key=\"endTimeAt\"\n        type=\"kobject.calendly_tutorial-event\"\n        value={object.custom.endTimeAt}\n      />\n      </Column>\n    <Column size=\"eight\">\n      <h4>Cancelation Details</h4>\n      <Field\n        field=\"canceledStr\"\n        key=\"canceledStr\"\n        type=\"kobject.calendly_tutorial-event\"\n        value={object.custom.canceledStr}\n      />\n      <Field\n        field=\"cancelReasonStr\"\n        key=\"cancelReasonStr\"\n        type=\"kobject.calendly_tutorial-event\"\n        value={object.custom.cancelReasonStr}\n      />\n   <Field\n        field=\"canceledDateStr\"\n        key=\"canceledDateStr\"\n        type=\"kobject.calendly_tutorial-event\"\n        value={object.custom.canceledDateStr}\n      />\n      </Column>\n  </Grid>\n  <Grid>\n    <Column size=\"sixteen\">\n      <h4>Questions / Answers</h4>\n      {(() => {\n        \tconst lineItems = _.get(data, 'kobject.data.payload.questions_and_answers');\n          return lineItems.map((lineItem) => {\n          \treturn (<div>\n            \t<Grid>\n             \t\t<Column size=\"sixteen\">\n                \t<BasicField label=\"Question\" value={_.get(lineItem, 'question')}/>\n                  <BasicField label=\"Answer\" value={_.get(lineItem, 'answer')}/>\n                </Column>\n             </Grid>\n            </div>)\n          })\n    \t})()}\n    </Column>\n  </Grid>\n</Segment>\n    :\n  null\n   }\n</div>",
     context: 'expanded-timeline',
     meta: {
       displayName: `${tutorialApp}-event`,
       icon: 'calendar',
       state: 'open'
     },
     name: `kustomer.app.${tutorialApp}.calendly-event-card`
   }
 ],

Step 10: Add a workflow

Workflows are automations in Kustomer that combine triggers and conditional logic to to perform a workflow action.

Learn more in Workflow Actions and Workflows Overview.

In our Calendly app, we will use a workflow to process data sent to the inbound webhook in Kustomer that we added as a part of the app. The workflow will be activated by our newly-added trigger.

The workflow will do the following:

  • When an event is received from Calendly, we will create a KObject representing the Calendly event, or update the KObject if it already exists. To accomplish this, we’ll make a few requests to the Calendly API to get some necessary information, then execute the rest of our logic.
  • To create the KObject, we’ll use the email of the invitee sent on the event to check for an existing customer.
    • If a customer exists, we will create the KObject with that customer.
    • If the customer does not exist, a new customer will be created, and the KObject will be created with the newly-created customer.

👍

Use our workflow editor

Fortunately, you don't have to build the workflow definition using only JSON. Instead, you can find a workflow editor in the Kustomer app that you can use to generate the workflow JSON definition.

The workflow editor makes it easy to specify the trigger, any necessary conditionals, and all workflow actions. The reason we installed the app scaffold in Step 8 was so that you'll be able to take advantage of the editor interface within Kustomer.

First, create and install the app (if you skipped Step 8 earlier). Then in Kustomer go to Settings > Workflows > Add Workflow to create a workflow using the actions and triggers installed by the app. Once the workflow is built, you can use the menu at the top of the editor to find the Edit Workflow JSON option, which will provide you with the workflow in JSON code.

Learn more about using the workflow editor in our help article Workflows Overview.

For the Calendly app, our workflow definition will look similar to what you see below:

workflows: [
   {
     description: 'Ingest a Calendly event',
     name: `${tutorialApp}-event-ingest`,
     steps: [
       {
         transitions: [
           {
             target: '46xBZv_tF',
             condition: {
               op: 'true',
               values: [
                 true
               ]
             }
           }
         ],
         errorCases: [],
         id: 'BpDFXPsb5',
         action: `kustomer.app.${tutorialApp}.get-request`,
         params: {
           uri: '/#steps.1.attributes.data.payload.event'
         },
         meta: {
           displayName: 'Get Calendly Event'
         },
         appVersion: `${tutorialApp}-^0.0.1`
       },
       {
         transitions: [
           {
             target: 'rK1vQHMaD',
             condition: {
               op: 'exists',
               values: [
                 '/#steps.BpDFXPsb5.body.resource.uri',
                 ''
               ]
             },
             meta: {
               name: 'Verify Calendly kObj'
             }
           }
         ],
         errorCases: [],
         id: '46xBZv_tF',
         action: `kustomer.app.${tutorialApp}.get-request`,
         params: {
           uri: '/#steps.BpDFXPsb5.body.resource.event_type'
         },
         meta: {
           displayName: 'Get Calendly Event Type'
         },
         appVersion: `${tutorialApp}-^0.0.1`
       },
       {
         transitions: [
           {
             target: 'r9YGZ3ejx',
             condition: {
               op: 'exists',
               values: [
                 '/#steps.Jzczqc68I.id',
                 ''
               ]
             },
             meta: {
               name: 'Customer Exists'
             }
           },
           {
             target: 'xjO-rQ7rU',
             condition: {
               op: 'dne',
               values: [
                 '/#steps.Jzczqc68I.id',
                 ''
               ]
             },
             meta: {
               name: 'Customer DNE'
             }
           }
         ],
         errorCases: [],
         id: 'Jzczqc68I',
         action: 'kustomer.customer.find-by-email',
         params: {
           email: '/#steps.CMkWb1SV4.match'
         },
         appVersion: 'kustomer-^1.5.0'
       },
       {
         transitions: [
           {
             target: '1bp0PtBou',
             condition: {
               op: 'true',
               values: [
                 true
               ]
             }
           }
         ],
         errorCases: [],
         id: 'rK1vQHMaD',
         action: 'kustomer.kobject.find-by-external-id',
         params: {
           klassName: `${tutorialApp}-event`,
           externalId: '/#fn:slice,steps.1.attributes.data.payload.uri,68'
         },
         appVersion: 'kustomer-^1.5.0'
       },
       {
         transitions: [
           {
             target: '4okgDunmS',
             condition: {
               op: 'exists',
               values: [
                 '/#steps.rK1vQHMaD.id',
                 ''
               ]
             },
             meta: {
               name: 'kObj Exists'
             }
           },
           {
             target: 'uxf949_lN',
             condition: {
               op: 'dne',
               values: [
                 '/#steps.rK1vQHMaD.id',
                 ''
               ]
             },
             meta: {
               name: 'kObj DNE'
             }
           }
         ],
         errorCases: [],
         id: '1bp0PtBou',
         meta: {
           displayName: 'Get Event'
         }
       },
       {
         transitions: [
           {
             target: 'CMkWb1SV4',
             condition: {
               op: 'true',
               values: [
                 true
               ]
             }
           }
         ],
         errorCases: [],
         id: 'uxf949_lN'
       },
       {
         transitions: [],
         errorCases: [],
         id: '4okgDunmS',
         action: 'kustomer.kobject.update',
         params: {
           klassName: `${tutorialApp}-event`,
           id: '/#steps.rK1vQHMaD.id',
           title: '/#steps.BpDFXPsb5.body.resource.name',
           data: '/#steps.1.attributes.data',
           custom: {
             eventTypeStr: '/#steps.46xBZv_tF.body.resource.type',
             eventNameStr: '/#steps.46xBZv_tF.body.resource.name',
             eventDescriptionStr: '/#steps.46xBZv_tF.body.resource.description_plain',
             eventDurationNum: '/#steps.46xBZv_tF.body.resource.duration',
             eventLocationStr: '{{#if steps.BpDFXPsb5.body.resource.location}}{{{steps.BpDFXPsb5.body.resource.location.location}}}{{else}}No location specified{{/if}}',
             canceledStr: '{{#if steps.1.attributes.data.payload.cancellation}}Yes{{else}}No{{/if}}',
             cancelReasonStr: '{{#if steps.1.attributes.data.payload.cancellation}}{{{steps.1.attributes.data.payload.cancellation.reason}}}{{else}}N/A{{/if}}',
             canceledDateStr: '{{#if steps.1.attributes.data.payload.cancellation }}{{ dateFormat steps.1.attributes.data.payload.updated_at format="ddd, MMM D YYYY, h:mm A" }}{{else}}N/A{{/if}}',
             startTimeAt: '/#steps.BpDFXPsb5.body.resource.start_time',
             endTimeAt: '/#steps.BpDFXPsb5.body.resource.end_time'
           }
         },
         appVersion: 'kustomer-^1.5.0'
       },
       {
         transitions: [
           {
             target: 'xVfVOeqw5',
             condition: {
               op: 'true',
               values: [
                 true
               ]
             }
           }
         ],
         errorCases: [],
         id: 'xjO-rQ7rU',
         action: 'kustomer.customer.create',
         appVersion: 'kustomer-^1.5.0',
         params: {
           email: '/#steps.CMkWb1SV4.match',
           name: '/#steps.1.attributes.data.payload.name'
         }
       },
       {
         transitions: [],
         errorCases: [],
         id: 'xVfVOeqw5',
         action: 'kustomer.kobject.create-with-customer',
         params: {
           klassName: 'calendly_tutorial-event',
           customer: '/#steps.xjO-rQ7rU.id',
           title: '/#steps.BpDFXPsb5.body.resource.name',
           data: '/#steps.1.attributes.data',
           externalId: '/#fn:slice,steps.1.attributes.data.payload.uri,68',
           custom: {
             eventTypeStr: '/#steps.46xBZv_tF.body.resource.type',
             eventNameStr: '/#steps.46xBZv_tF.body.resource.name',
             eventDescriptionStr: '/#steps.46xBZv_tF.body.resource.description_plain',
             eventDurationNum: '/#steps.46xBZv_tF.body.resource.duration',
             eventLocationStr: '{{#if steps.BpDFXPsb5.body.resource.location}}{{{steps.BpDFXPsb5.body.resource.location.location}}}{{else}}No location specified{{/if}}',
             canceledStr: '{{#if steps.1.attributes.data.payload.cancellation}}Yes{{else}}No{{/if}}',
             cancelReasonStr: '{{#if steps.1.attributes.data.payload.cancellation}}{{{steps.1.attributes.data.payload.cancellation.reason}}}{{else}}N/A{{/if}}',
             canceledDateStr: '{{#if steps.1.attributes.data.payload.cancellation }}{{ dateFormat steps.1.attributes.data.payload.updated_at format="ddd, MMM D YYYY, h:mm A" }}{{else}}N/A{{/if}}',
             startTimeAt: '/#steps.BpDFXPsb5.body.resource.start_time',
             endTimeAt: '/#steps.BpDFXPsb5.body.resource.end_time'
           }
         },
         appVersion: 'kustomer-^1.5.0'
       },
       {
         transitions: [],
         errorCases: [],
         id: 'r9YGZ3ejx',
         action: 'kustomer.kobject.create-with-customer',
         params: {
           klassName: `${tutorialApp}-event`,
           customer: '/#steps.Jzczqc68I.id',
           title: '/#steps.BpDFXPsb5.body.resource.name',
           data: '/#steps.1.attributes.data',
           externalId: '/#fn:slice,steps.1.attributes.data.payload.uri,68',
           custom: {
             eventTypeStr: '/#steps.46xBZv_tF.body.resource.type',
             eventNameStr: '/#steps.46xBZv_tF.body.resource.name',
             eventDescriptionStr: '/#steps.46xBZv_tF.body.resource.description_plain',
             eventDurationNum: '/#steps.46xBZv_tF.body.resource.duration',
             eventLocationStr: '{{#if steps.BpDFXPsb5.body.resource.location}}{{{steps.BpDFXPsb5.body.resource.location.location}}}{{else}}No location specified{{/if}}',
             canceledStr: '{{#if steps.1.attributes.data.payload.cancellation}}Yes{{else}}No{{/if}}',
             cancelReasonStr: '{{#if steps.1.attributes.data.payload.cancellation}}{{{steps.1.attributes.data.payload.cancellation.reason}}}{{else}}N/A{{/if}}',
             canceledDateStr: '{{#if steps.1.attributes.data.payload.cancellation }}{{ dateFormat steps.1.attributes.data.payload.updated_at format="ddd, MMM D YYYY, h:mm: A" }}{{else}}N/A{{/if}}',
             startTimeAt: '/#steps.BpDFXPsb5.body.resource.start_time',
             endTimeAt: '/#steps.BpDFXPsb5.body.resource.end_time'
           }
         },
         appVersion: 'kustomer-^1.5.0'
       },
       {
         transitions: [
           {
             target: 'Jzczqc68I',
             condition: {
               op: 'and',
               values: [
                 {
                   op: 'exists',
                   values: [
                     '/#steps.CMkWb1SV4.match',
                     ''
                   ]
                 },
                 {
                   op: 'eq',
                   values: [
                     '/#fn:isValidEmailAddress,steps.CMkWb1SV4.match',
                     'true'
                   ]
                 }
               ]
             },
             meta: {
               name: 'Valid Email?'
             }
           }
         ],
         errorCases: [],
         id: 'CMkWb1SV4',
         action: 'kustomer.regex-match.generic',
         params: {
           testString: '{{#if steps.1.attributes.data.payload.email}}{{{steps.1.attributes.data.payload.email}}}{{else}}no email{{/if}}',
           regex: '(.*)'
         },
         meta: {
           displayName: 'Regex Email'
         },
         appVersion: 'kustomer-^1.5.0'
       }
     ],
     trigger: {
       transitions: [
         {
           target: 'BpDFXPsb5',
           condition: {
             op: 'true',
             values: [
               true
             ]
           }
         }
       ],
       eventName: `kustomer.app.${tutorialApp}.update-event`,
       id: '1',
       appVersion: `${tutorialApp}-^0.0.1`
     }
   }
 ]

Step 11: Test the app

Now that we have a completed app definition, we can register a new version of the Calendly app that includes the new KView and workflow we just created. This is how you'll test the app and make sure it looks good to go before sharing it with your team.

  1. Change the app version from 0.0.1. to 1.0.0 in the app definition and workflow references.
  2. Register the new app definition on the /v1/apps/available/ endpoint.
  3. Install the app on your Kustomer org to test.
  4. Follow the instructions in Integrate with Calendly to set up your integration and create the webhook subscription.
  5. Now that the integration is set up, create a meeting in Calendly by going to https://calendly.com/event_types/user/me and clicking View booking page in the 15 Minute Meeting section. Enter a time, name, and email, then click Schedule Event.

This should result in Calendly sending a webhook to your inbound webhook URL in Kustomer. The webhook call will trigger the workflow, process the data, and create the KObject. The custom object will shown in the KView we created, and will be shown on your Kustomer timeline for the customer associated with the email you used to create the event.