Custom Workflows

What would you do if you could write your own workflow?


Custom Workflows supercharge the customization and automation of your calendaring experience.

Like other Morgen Assist Workflows, they enable control over your events, meetings, and scheduled tasks.

The twist? You write it, and we run it for you!

Custom Workflows are blank canvases: you can write your own code, automate your O365/Google/Slack via our dedicated APIs, and integrate it with third-party apps.

Once your workflow is ready to be deployed, our system will trigger it just like any other workflow on our platform. Triggers include calendar updates and periodical, or you can launch your workflow manually from the platform or externally via HTTP.

Here are some examples of what you can implement:

  • 🤖 Fancy auto-scheduling tomorrow's tasks from your third-party task manager? Schedule tasks aligned with your true availability.

  • 💬 Dream of a Slack notification for every invite from VIP email addresses? We've got you.

  • 🚫 Don't want meetings at certain times or days? Auto-decline them.

  • 💌 How about a Friday roundup? Make your team aware of time spent in long meetings in the past week, slacking them directly any useful stats.

  • 🚀 Release something? Trigger a workflow directly from the deployment script to set a reminder for your analyst 3 days later, ensuring they peek at the metrics.

Create your first Custom Workflow

Sign in to Morgen Assist:

Create a new account or sign in to Morgen Assist (opens in a new tab). If you already have a Morgen account, you can use it to sign in to Morgen Assist.

Connect your calendars:

Before proceeding to create Custom Workflows, ensure that you've connected the calendar accounts you wish to control. This ensures your workflow has the visibility and permissions it needs.

Navigate to the Workflows Gallery:

In the workflows gallery, you'll find the Custom Workflow together with the other workflows. Click on create or go directly to the Custom Workflow (opens in a new tab) create page.

Create a new Custom Workflow:

Here you can create a new Custom Workflow, you can:

  • Choose a name for your workflow.
  • Choose the calendar you want to trigger the workflow. When any event is added, updated or removed, the workflow will run.
  • Select the calendars you want control over within your script.
  • Provide your own script to run when the workflow is triggered.

Next Steps

Now that you've got the basics down, check out the more advanced examples below or check out the documentation on the Morgen API.

Alternatively, you might want to step out of the web app and back into the comfort of your own dev environment with the Morgen Custom Workflow SDK (opens in a new tab)

Data Types

Here are some data type references you might find useful in the development of your first workflows:

Trigger Data

The trigger argument is always passed to your script when the workflow is triggered.

async function run(trigger) {
  const {
    // Changes to the specified calendar since the previous trigger
    // Each field is an array of Event[]
    eventUpdates: { added, removed, modified },
    accounts: {
      // An array of calendars provided by workflow configuration
      // E.g. [{accountId: "...", calendarId: "..."}]
    // Your user object
    user: {
      // The primary email address associated with your account
      // Your first name
  } = trigger;

Event from eventUpdates

The event object returned from eventUpdates. See the JSCalendar docs (opens in a new tab) for details.

    "@type": "Event",
    // Morgen ID for event
    "id": "WyJkODQzNjAzOTljZ...",
    // 3rd-party ID
    "uid": "",
    // Morgen ID for the account (not the user's ID, but the 3rd party account)
    "accountId": "6441...d83a",
    // 3rd-party ID for the base event in the recurrence
    "baseEventId": "kki3mce...0_20230829T140000Z",
    // Morgen ID for the calendar
    "calendarId": "WyI2NDQxNjgzODU5Y2Y...",
    // Either "google" or "o365"
    "integrationId": "google",
    // baseEventId of the parent recurring event
    "masterBaseEventId": "kki3mce...",
    // ID + timezone that identify this event within the recurrence
    "recurrenceId": "2023-08-29T16:00:00",
    "recurrenceIdTimeZone": "Europe/Zurich",
    // Timestamp of when the event was created
    "created": "2023-08-29T10:20:59.000Z",
    // Description provided by the user
      "This is the description!\n\n" +
      "Here's a <em>link -&gt;</em> <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"\"></a>" +
      "Join Meeting\n\n\n",
    // Either "text/plain" or "text/html" depending on content
    "descriptionContentType": "text/html",
    // Format: 'PT[hours]H' | 'PT[minutes]M'
    "duration": "PT1H",
    // Whether the event is marked "busy" for the participants. Otherwise "free".
    "freeBusyStatus": "busy",
    // Whether the details can be displayed publically to others (in Google/O365 apps)
    "privacy": "public",
    // If true, this event is an all-day event. Otherwise it occurs at a specific time in the day.
    "showWithoutTime": false,
    // Time at which the event will begin
    "start": "2023-08-29T16:00:00",
    "timeZone": "Europe/Zurich",
    // The title of the event
    "title": "An example event",
    // Timestamp indicating last time event was changed
    "updated": "2023-08-29T13:14:57.079Z",
    "alerts": {
      // Alert for 30 minutes before the start of the event
      "0": {
        "@type": "Alert",
        "action": "display",
        "trigger": {
          "@type": "OffsetTrigger",
          "offset": "-PT30M",
          "relativeTo": "start"
    "locations": {
      "1": {
        "@type": "Location",
        "name": "Zurich HB, Bahnhofpl., 8001 Zürich, Switzerland"
    "participants": {
      // Map of pariticpant IDs to participants
      "ZDg0MzYwMzk5Y2...": {
        "@type": "Participant",
        // Indicates whether this participant is also the owner of
        // the calendar account
        "accountOwner": true,
        // Email address of the participant
        // NOTE: Sometimes, the participant email is a group email,
        //       e.g. if the event was created in a secondary Google
        //       calendar.
        "email": "",
        // Participant's full name
        // NOTE: For secondary calendars, this is the name of the calendar
        "name": "A great calendar",
        "roles": {
          // Whether this participant will be attending
          "attendee": true,
          // Whether the participant owns the event and can
          // therefore make changes that affect other
          // participants
          "owner": true
      // An invited participant
      "bHVrZS50ZXN0a...": {
        "@type": "Participant",
        // Indicates whether this participant is also the owner of the calendar account
        "accountOwner": true,
        // Email of the participant
        "email": "",
        // For attendees that have been sent an invitation,
        // this indicates whether they have responded to the
        // invitation or whether they need to respond.
        "participationStatus": "needs-action",
        "roles": {
          "attendee": true


Here are some utility functions you can use in your custom workflow scripts:

Wrapper for fetching Morgen APIs

This wrapper will make it easier to make requests to Morgen APIs using the fetch interface (opens in a new tab). It uses the global TOKEN variable, which stores your own personal access token to make calls to our API. You can use this to make requests to for:

  • fetching/updating calendar events and tasks
  • interacting with your other accounts
async function fetchMorgen(url, opts) {
  return fetch(
      headers: {
        Authorization: `Bearer ${JSON.parse(global.TOKEN)}`,
        Accept: 'application/json',
        ["Content-Type"]: 'application/json',

Get a ChatGPT prompt response

The code snippet below demonstrates how to use the getOpenAIResponse function to generate chat responses using the OpenAI API. Before using this code, make sure you have your OpenAI API token.

async function getOpenAIResponse(prompt) {
  const resp = fetch(
      method: 'POST',
      headers: {
        Authorization: 'Bearer [YOUR-TOKEN]',
        Accepts: 'application/json',
        'Content-Type': 'application/json',
      body: JSON.stringify({
        "model": "gpt-3.5-turbo",
        "messages": [{
          "role": "user",
          "content": prompt
        "temperature": 0.7
  const parsed = JSON.parse(resp);
  if (parsed.error) {
    throw new Error('Error with OpenAI: ' + JSON.stringify(parsed.error));
  return parsed.choices[0].message.content;

Custom Workflows Examples

Here are some examples of custom workflows that you can create:

Create an Event via Morgen API

The following code snippet shows how to use the fetchMorgen utility to create a new event using the Morgen API.

async function fetchMorgen(url, opts) { ... } // See Utilities: Wrapper for fetching Morgen APIs
async function run(trigger) {
  const { accountId, calendarId } = trigger.accounts.calendar[0];
  const resp = await fetchMorgen("", {
    method: 'POST',
    body: JSON.stringify({
      title: 'My New Event',
      description: '#automated',
      start: "2023-08-25T13:00:00",
      timeZone: "UTC",
      showWithoutTime: false,
      duration: "PT1H"
  log('Response received: ' + JSON.stringify(resp, null, 2));

Send a Slack Message

The following code snippet shows how to receive a Slack message when something changes in your calendar.

 async function run(trigger) {
    const slackToken = "xorb-[your slack bot access token]";
    const resp = await fetch(
      "", {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${slackToken}`,
        Accept: 'application/json',
        ["Content-Type"]: 'application/json',
      body: JSON.stringify({
        channel: '@Luke',
        text: 'Hi there, something changed in your calendar!'

Custom Workflow: Generate event descriptions using ChatGPT

The following code snippet shows how to use the fetchMorgen and getOpenAIResponse to put ChatGPT to work on your newly created events.

It uses the prompt "Write a movie trailer script for a movie of this title" to write a new event description via ChatGPT, and passes the result to the event update API.

async function fetchMorgen(url, opts) { ... } // See Utilities: Wrapper for fetching Morgen APIs
async function getOpenAIResponse(prompt) { ... } // See Utilities: Get a ChatGPT prompt response
async function run(trigger) {
  const { accountId, calendarId } = trigger.accounts.calendar[0];
  // Only process newly added events - ignore updates.
  // NOTE: Important to ignore the calendar update later in the script
  if (trigger.eventUpdates.modified.length > 0) {
  // Get the first added event that hasn't been processed previously (ignore the rest)
  const addedEvent = trigger.eventUpdates.added[0];
  // Generate a description using the title of the event
  const title = addedEvent.title;
  const description = await getOpenAIResponse("Write a movie trailer script for a movie of this title: " + title);
  // Update the event via the Morgen API
  const resp = await fetchMorgen("", {
    method: 'POST',
    body: JSON.stringify({
  log('Event updated: ' + JSON.stringify(resp, null, 2));