Repository: Accenture/sfmc-devtools (js)

Issue:
[FEATURE] support getting+setting automation notification email address
**Is your feature request related to a problem? Please describe.**
As of now, we do not include that info in our retrieve/upsert. This means we have to go and set that manually after each deployment.

**Describe the solution you'd like**
Be able to set error & confirmation notification email address.

- **Error or skipped run**: you will receive an email if the automation’s execution fails or is skipped
- **Run Completion**: you will receive an email once the automation’s run completes.


## infos
- **retrieve** notifications info via SOAP https://charliefay.medium.com/search-all-automations-for-a-notification-email-address-1cb0aa18a919
- **Create** automation including notifications: https://sfmarketing.cloud/2019/12/08/subscribe-to-automation-studio-notifications-via-soap-api/
- **Retrieve**, **Create** & **Update** via REST: https://gortonington.com/sfmc-automation-notifications-via-rest-api/

Pull request description:
Feature/553 support getting+setting automation notification email
# PR details

## What changes did you make? (Give an overview)

- closes #553 

## Checklist

- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] test scripts updated
- [x] Wiki updated (via release PR #961)



Patch:
diff --git a/docs/dist/documentation.md b/docs/dist/documentation.md
index be608d2e..88cf4377 100644
--- a/docs/dist/documentation.md
+++ b/docs/dist/documentation.md
@@ -186,6 +186,12 @@ Provides default functionality that can be overwritten by child metadata type cl
 <dt><a href="#csvToArray">csvToArray(csv)</a> ⇒ <code>Array.&lt;string&gt;</code></dt>
 <dd><p>helper to convert CSVs into an array. if only one value was given, it&#39;s also returned as an array</p>
 </dd>
+<dt><a href="#Automation.">Automation.(metadataMap, key)</a> ⇒ <code>Promise.&lt;void&gt;</code></dt>
+<dd><p>helper for <a href="#Automation.postDeployTasks">postDeployTasks</a></p>
+</dd>
+<dt><a href="#Automation.">Automation.(metadataMap, originalMetadataMap, key)</a></dt>
+<dd><p>helper for <a href="postDeployTasks">postDeployTasks</a></p>
+</dd>
 <dt><a href="#getUserName">getUserName(userList, item, fieldname)</a> ⇒ <code>string</code></dt>
 <dd></dd>
 <dt><a href="#setupSDK">setupSDK(sessionKey, authObject)</a> ⇒ <code><a href="#SDK">SDK</a></code></dt>
@@ -5885,7 +5891,7 @@ CLI entry for SFMC DevTools
     * [.includesStartsWith(arr, search)](#Util.includesStartsWith) ⇒ <code>boolean</code>
     * [.includesStartsWithIndex(arr, search)](#Util.includesStartsWithIndex) ⇒ <code>number</code>
     * [.checkMarket(market, properties)](#Util.checkMarket) ⇒ <code>boolean</code>
-    * [.verifyMarketList(mlName, properties)](#Util.verifyMarketList) ⇒ <code>void</code>
+    * [.verifyMarketList(mlName, properties)](#Util.verifyMarketList)
     * [.signalFatalError()](#Util.signalFatalError) ⇒ <code>void</code>
     * [.isTrue(attrValue)](#Util.isTrue) ⇒ <code>boolean</code>
     * [.isFalse(attrValue)](#Util.isFalse) ⇒ <code>boolean</code>
@@ -5897,7 +5903,7 @@ CLI entry for SFMC DevTools
     * [.inverseGet(objs, val)](#Util.inverseGet) ⇒ <code>string</code>
     * [.getMetadataHierachy(metadataTypes)](#Util.getMetadataHierachy) ⇒ <code>Object.&lt;string, Array.&lt;string&gt;&gt;</code>
     * [.resolveObjPath(path, obj)](#Util.resolveObjPath) ⇒ <code>any</code>
-    * [.execSync(cmd, [args], [hideOutput])](#Util.execSync) ⇒ <code>string</code>
+    * [.execSync(cmd, [args], [hideOutput])](#Util.execSync) ⇒ <code>string</code> \| <code>void</code>
     * [.templateSearchResult(results, keyToSearch, searchValue)](#Util.templateSearchResult) ⇒ <code>TYPE.MetadataTypeItem</code>
     * [.setLoggingLevel(argv)](#Util.setLoggingLevel) ⇒ <code>void</code>
     * [.logBeta(type)](#Util.logBeta)
@@ -5971,11 +5977,10 @@ check if a market name exists in current mcdev config
 
 <a name="Util.verifyMarketList"></a>
 
-### Util.verifyMarketList(mlName, properties) ⇒ <code>void</code>
+### Util.verifyMarketList(mlName, properties)
 ensure provided MarketList exists and it's content including markets and BUs checks out
 
 **Kind**: static method of [<code>Util</code>](#Util)  
-**Returns**: <code>void</code> - throws errors if problems were found  
 
 | Param | Type | Description |
 | --- | --- | --- |
@@ -6113,11 +6118,11 @@ let's you dynamically walk down an object and get a value
 
 <a name="Util.execSync"></a>
 
-### Util.execSync(cmd, [args], [hideOutput]) ⇒ <code>string</code>
+### Util.execSync(cmd, [args], [hideOutput]) ⇒ <code>string</code> \| <code>void</code>
 helper to run other commands as if run manually by user
 
 **Kind**: static method of [<code>Util</code>](#Util)  
-**Returns**: <code>string</code> - output of command if hideOutput is true  
+**Returns**: <code>string</code> \| <code>void</code> - output of command if hideOutput is true  
 
 | Param | Type | Description |
 | --- | --- | --- |
@@ -7750,7 +7755,7 @@ Util that contains logger and simple util methods
     * [.includesStartsWith(arr, search)](#Util.includesStartsWith) ⇒ <code>boolean</code>
     * [.includesStartsWithIndex(arr, search)](#Util.includesStartsWithIndex) ⇒ <code>number</code>
     * [.checkMarket(market, properties)](#Util.checkMarket) ⇒ <code>boolean</code>
-    * [.verifyMarketList(mlName, properties)](#Util.verifyMarketList) ⇒ <code>void</code>
+    * [.verifyMarketList(mlName, properties)](#Util.verifyMarketList)
     * [.signalFatalError()](#Util.signalFatalError) ⇒ <code>void</code>
     * [.isTrue(attrValue)](#Util.isTrue) ⇒ <code>boolean</code>
     * [.isFalse(attrValue)](#Util.isFalse) ⇒ <code>boolean</code>
@@ -7762,7 +7767,7 @@ Util that contains logger and simple util methods
     * [.inverseGet(objs, val)](#Util.inverseGet) ⇒ <code>string</code>
     * [.getMetadataHierachy(metadataTypes)](#Util.getMetadataHierachy) ⇒ <code>Object.&lt;string, Array.&lt;string&gt;&gt;</code>
     * [.resolveObjPath(path, obj)](#Util.resolveObjPath) ⇒ <code>any</code>
-    * [.execSync(cmd, [args], [hideOutput])](#Util.execSync) ⇒ <code>string</code>
+    * [.execSync(cmd, [args], [hideOutput])](#Util.execSync) ⇒ <code>string</code> \| <code>void</code>
     * [.templateSearchResult(results, keyToSearch, searchValue)](#Util.templateSearchResult) ⇒ <code>TYPE.MetadataTypeItem</code>
     * [.setLoggingLevel(argv)](#Util.setLoggingLevel) ⇒ <code>void</code>
     * [.logBeta(type)](#Util.logBeta)
@@ -7836,11 +7841,10 @@ check if a market name exists in current mcdev config
 
 <a name="Util.verifyMarketList"></a>
 
-### Util.verifyMarketList(mlName, properties) ⇒ <code>void</code>
+### Util.verifyMarketList(mlName, properties)
 ensure provided MarketList exists and it's content including markets and BUs checks out
 
 **Kind**: static method of [<code>Util</code>](#Util)  
-**Returns**: <code>void</code> - throws errors if problems were found  
 
 | Param | Type | Description |
 | --- | --- | --- |
@@ -7978,11 +7982,11 @@ let's you dynamically walk down an object and get a value
 
 <a name="Util.execSync"></a>
 
-### Util.execSync(cmd, [args], [hideOutput]) ⇒ <code>string</code>
+### Util.execSync(cmd, [args], [hideOutput]) ⇒ <code>string</code> \| <code>void</code>
 helper to run other commands as if run manually by user
 
 **Kind**: static method of [<code>Util</code>](#Util)  
-**Returns**: <code>string</code> - output of command if hideOutput is true  
+**Returns**: <code>string</code> \| <code>void</code> - output of command if hideOutput is true  
 
 | Param | Type | Description |
 | --- | --- | --- |
@@ -8116,6 +8120,32 @@ helper to convert CSVs into an array. if only one value was given, it's also ret
 | --- | --- | --- |
 | csv | <code>string</code> | potentially comma-separated value or null |
 
+<a name="Automation."></a>
+
+## Automation.(metadataMap, key) ⇒ <code>Promise.&lt;void&gt;</code>
+helper for [postDeployTasks](#Automation.postDeployTasks)
+
+**Kind**: global function  
+**Returns**: <code>Promise.&lt;void&gt;</code> - -  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| metadataMap | <code>TYPE.AutomationMap</code> | metadata mapped by their keyField |
+| key | <code>string</code> | current customer key |
+
+<a name="Automation."></a>
+
+## Automation.(metadataMap, originalMetadataMap, key)
+helper for [postDeployTasks](postDeployTasks)
+
+**Kind**: global function  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| metadataMap | <code>TYPE.AutomationMap</code> | metadata mapped by their keyField |
+| originalMetadataMap | <code>TYPE.AutomationMap</code> | metadata to be updated (contains additioanl fields) |
+| key | <code>string</code> | current customer key |
+
 <a name="getUserName"></a>
 
 ## getUserName(userList, item, fieldname) ⇒ <code>string</code>
diff --git a/lib/metadataTypes/Automation.js b/lib/metadataTypes/Automation.js
index f404e6b4..560ed94e 100644
--- a/lib/metadataTypes/Automation.js
+++ b/lib/metadataTypes/Automation.js
@@ -6,6 +6,7 @@ const Util = require('../util/util');
 const File = require('../util/file');
 const Definitions = require('../MetadataTypeDefinitions');
 const cache = require('../util/cache');
+const pLimit = require('p-limit');
 
 /**
  * Automation MetadataType
@@ -13,6 +14,7 @@ const cache = require('../util/cache');
  * @augments MetadataType
  */
 class Automation extends MetadataType {
+    static notificationUpdates = {};
     /**
      * Retrieves Metadata of Automation
      *
@@ -39,7 +41,7 @@ class Automation extends MetadataType {
             // empty results will come back without "Results" defined
             Util.logger.info(
                 Util.getGrayMsg(
-                    ` - ${results.Results?.length} Automation${
+                    ` - ${results.Results?.length} automation${
                         results.Results?.length === 1 ? '' : 's'
                     } found. Retrieving details...`
                 )
@@ -73,6 +75,12 @@ class Automation extends MetadataType {
               )
             : [];
         let metadataMap = this.parseResponseBody({ items: details });
+
+        if (Object.keys(metadataMap).length) {
+            // attach notification information to each automation that has any
+            await this.#getAutomationNotificationsREST(metadataMap);
+        }
+
         // * retrieveDir can be empty when we use it in the context of postDeployTasks
         if (retrieveDir) {
             metadataMap = await this.saveResults(metadataMap, retrieveDir, null, null);
@@ -85,6 +93,77 @@ class Automation extends MetadataType {
         }
         return { metadata: metadataMap, type: this.definition.type };
     }
+
+    /**
+     * helper for {@link Automation.retrieve} to get Automation Notifications
+     *
+     * @private
+     * @param {TYPE.MetadataTypeMap} metadataMap keyField => metadata map
+     * @returns {Promise.<void>} Promise of nothing
+     */
+    static async #getAutomationNotificationsREST(metadataMap) {
+        Util.logger.info(Util.getGrayMsg(` Retrieving Automation Notification information...`));
+
+        // get list of keys that we retrieved so far
+        const foundKeys = Object.keys(metadataMap);
+
+        // get encodedAutomationID to retrieve notification information
+        const iteratorBackup = this.definition.bodyIteratorField;
+        this.definition.bodyIteratorField = 'entry';
+        const automationLegacyMapObj = await super.retrieveREST(
+            undefined,
+            `/legacy/v1/beta/bulk/automations/automation/definition/`
+        );
+        this.definition.bodyIteratorField = iteratorBackup;
+        const automationLegacyMap = Object.keys(automationLegacyMapObj.metadata)
+            .filter((key) => foundKeys.includes(key))
+            // ! using the `id` field to retrieve notifications does not work. instead one needs to use the URL in the `notifications` field
+            .map((key) => ({
+                id: automationLegacyMapObj.metadata[key].id,
+                key,
+            }));
+
+        // get notifications for each automation
+        const rateLimit = pLimit(5);
+        let found = 0;
+        let skipped = 0;
+        const promiseMap = await Promise.all(
+            automationLegacyMap.map((automationLegacy) =>
+                rateLimit(async () => {
+                    // this is a file so extended is at another endpoint
+                    try {
+                        const notificationsResult = await this.client.rest.get(
+                            '/legacy/v1/beta/automations/notifications/' + automationLegacy.id
+                        );
+                        if (Array.isArray(notificationsResult?.workers)) {
+                            metadataMap[automationLegacy.key].notifications =
+                                notificationsResult.workers.map((n) => ({
+                                    email: n.definition.split(',').map((item) => item.trim()),
+                                    message: n.body,
+                                    type: n.notificationType,
+                                }));
+                            found++;
+                        } else {
+                            throw new TypeError(JSON.stringify(notificationsResult));
+                        }
+                    } catch (ex) {
+                        Util.logger.debug(
+                            ` ☇ skipping Notifications for Automation ${automationLegacy.key}: ${ex.message} ${ex.code}`
+                        );
+                        skipped++;
+                    }
+                })
+            )
+        );
+        Util.logger.info(
+            Util.getGrayMsg(` Notifications found for ${found} automation${found === 1 ? '' : 's'}`)
+        );
+        Util.logger.debug(
+            `Notifications not found for ${skipped} automation${skipped === 1 ? '' : 's'}`
+        );
+        return promiseMap;
+    }
+
     /**
      * Retrieves Metadata of Automation
      *
@@ -136,6 +215,7 @@ class Automation extends MetadataType {
      * @returns {Promise.<TYPE.AutomationMapObj>} Promise of metadata
      */
     static async retrieveForCache() {
+        // get automations for cache
         const results = await this.client.soap.retrieveBulk('Program', [
             'ObjectID',
             'CustomerKey',
@@ -143,14 +223,29 @@ class Automation extends MetadataType {
         ]);
         const resultsConverted = {};
         if (Array.isArray(results?.Results)) {
+            // get encodedAutomationID to retrieve notification information
+            const keyBackup = this.definition.keyField;
+            const iteratorBackup = this.definition.bodyIteratorField;
+            this.definition.keyField = 'key';
+            this.definition.bodyIteratorField = 'entry';
+            const automationsLegacy = await super.retrieveREST(
+                undefined,
+                `/legacy/v1/beta/bulk/automations/automation/definition/`
+            );
+            this.definition.keyField = keyBackup;
+            this.definition.bodyIteratorField = iteratorBackup;
+
+            // merge encodedAutomationID into results
             for (const m of results.Results) {
                 resultsConverted[m.CustomerKey] = {
                     id: m.ObjectID,
                     key: m.CustomerKey,
                     name: m.Name,
+                    programId: automationsLegacy.metadata[m.CustomerKey]?.id,
                 };
             }
         }
+
         return { metadata: resultsConverted, type: this.definition.type };
     }
 
@@ -177,9 +272,16 @@ class Automation extends MetadataType {
                 Util.logger.error(`${this.definition.type} '${name}' not found on server.`);
                 return;
             }
-            const details = await this.client.rest.get(
+            let details = await this.client.rest.get(
                 '/automation/v1/automations/' + metadata.ObjectID
             );
+            const metadataMap = this.parseResponseBody({ items: [details] });
+            if (Object.keys(metadataMap).length) {
+                // attach notification information to each automation that has any
+                await this.#getAutomationNotificationsREST(metadataMap);
+                details = Object.values(metadataMap)[0];
+            }
+
             let val = null;
             let originalKey;
             // if parsing fails, we should just save what we get
@@ -398,6 +500,15 @@ class Automation extends MetadataType {
      * @returns {Promise.<TYPE.AutomationItem>} Promise
      */
     static async preDeployTasks(metadata) {
+        if (metadata.notifications) {
+            this.notificationUpdates[metadata.key] = metadata.notifications;
+        } else {
+            const cached = cache.getByKey(metadata.key);
+            if (cached?.notifications) {
+                // if notifications existed but are no longer present in the deployment package, we need to run an empty update call to remove them
+                this.notificationUpdates[metadata.key] = [];
+            }
+        }
         if (this.validateDeployMetadata(metadata)) {
             // folder
             this.setFolderId(metadata);
@@ -532,86 +643,146 @@ class Automation extends MetadataType {
     static async postDeployTasks(metadataMap, originalMetadataMap) {
         for (const key in metadataMap) {
             // need to put schedule on here if status is scheduled
+            await Automation.#scheduleAutomation(metadataMap, originalMetadataMap, key);
 
-            if (originalMetadataMap[key]?.type === 'scheduled') {
-                // Starting Source == 'Schedule': Try starting the automation
-                if (originalMetadataMap[key].status === 'Scheduled') {
-                    let schedule = null;
+            // need to update notifications separately if there are any
+            await Automation.#updateNotificationInfoREST(metadataMap, key);
+
+            // rewrite upsert to retrieve fields
+            const metadata = metadataMap[key];
+            if (metadata.steps) {
+                for (const step of metadata.steps) {
+                    step.name = step.annotation;
+                    delete step.annotation;
+                }
+            }
+        }
+    }
+    /**
+     * helper for {@link Automation.postDeployTasks}
+     *
+     * @param {TYPE.AutomationMap} metadataMap metadata mapped by their keyField
+     * @param {string} key current customer key
+     * @returns {Promise.<void>} -
+     */
+    static async #updateNotificationInfoREST(metadataMap, key) {
+        if (this.notificationUpdates[key]) {
+            // create & update automation calls return programId as 'legacyId'; retrieve does not return it
+            const programId = metadataMap[key]?.legacyId;
+            if (programId) {
+                const notificationBody = {
+                    programId,
+                    workers: this.notificationUpdates[key].map((notification) => ({
+                        programId,
+                        notificationType: notification.type,
+                        definition: Array.isArray(notification.email)
+                            ? notification.email.join(',')
+                            : notification.email,
+                        body: notification.message,
+                        channelType: 'Account',
+                    })),
+                };
+                try {
+                    const result = await this.client.rest.post(
+                        '/legacy/v1/beta/automations/notifications/' + programId,
+                        notificationBody
+                    );
+                    if (result) {
+                        // should be empty if all OK
+                        throw new Error(result);
+                    }
+                } catch (ex) {
+                    Util.logger.error(
+                        `Error updating notifications for automation '${metadataMap[key].name}': ${ex.message} (${ex.code}))`
+                    );
+                }
+                Util.logger.info(
+                    Util.getGrayMsg(
+                        ` - updated notifications for automation '${metadataMap[key].name}'`
+                    )
+                );
+            }
+        }
+    }
+
+    /**
+     * helper for {@link postDeployTasks}
+     *
+     * @param {TYPE.AutomationMap} metadataMap metadata mapped by their keyField
+     * @param {TYPE.AutomationMap} originalMetadataMap metadata to be updated (contains additioanl fields)
+     * @param {string} key current customer key
+     */
+    static async #scheduleAutomation(metadataMap, originalMetadataMap, key) {
+        if (originalMetadataMap[key]?.type === 'scheduled') {
+            // Starting Source == 'Schedule': Try starting the automation
+            if (originalMetadataMap[key].status === 'Scheduled') {
+                let schedule = null;
+                try {
+                    schedule = this._buildSchedule(originalMetadataMap[key].schedule);
+                } catch (ex) {
+                    Util.logger.error(
+                        `- Could not create schedule for automation '${originalMetadataMap[key].name}' to start it: ${ex.message}`
+                    );
+                }
+                if (schedule !== null) {
                     try {
-                        schedule = this._buildSchedule(originalMetadataMap[key].schedule);
+                        // remove the fields that are not needed for the schedule but only for CLI output
+                        const schedule_StartDateTime = schedule._StartDateTime;
+                        delete schedule._StartDateTime;
+                        const schedule_interval = schedule._interval;
+                        delete schedule._interval;
+                        const schedule_timezoneString = schedule._timezoneString;
+                        delete schedule._timezoneString;
+                        // start the automation
+                        await this.client.soap.schedule(
+                            'Automation',
+                            schedule,
+                            {
+                                Interaction: {
+                                    ObjectID: metadataMap[key].id,
+                                },
+                            },
+                            'start',
+                            {}
+                        );
+                        const intervalString =
+                            (schedule_interval > 1 ? `${schedule_interval} ` : '') +
+                            (schedule.RecurrenceType === 'Daily'
+                                ? 'Day'
+                                : schedule.RecurrenceType.slice(0, -2) +
+                                  (schedule_interval > 1 ? 's' : ''));
+                        Util.logger.warn(
+                            ` - scheduled automation '${
+                                originalMetadataMap[key].name
+                            }' deployed as Active: runs every ${intervalString} starting ${
+                                schedule_StartDateTime.split('T').join(' ').split('.')[0]
+                            } ${schedule_timezoneString}`
+                        );
                     } catch (ex) {
                         Util.logger.error(
-                            `- Could not create schedule for automation ${originalMetadataMap[key].name} to start it: ${ex.message}`
+                            `- Could not start scheduled automation '${originalMetadataMap[key].name}': ${ex.message}`
                         );
                     }
-                    if (schedule !== null) {
-                        try {
-                            // remove the fields that are not needed for the schedule but only for CLI output
-                            const schedule_StartDateTime = schedule._StartDateTime;
-                            delete schedule._StartDateTime;
-                            const schedule_interval = schedule._interval;
-                            delete schedule._interval;
-                            const schedule_timezoneString = schedule._timezoneString;
-                            delete schedule._timezoneString;
-                            // start the automation
-                            await this.client.soap.schedule(
-                                'Automation',
-                                schedule,
-                                {
-                                    Interaction: {
-                                        ObjectID: metadataMap[key].id,
-                                    },
-                                },
-                                'start',
-                                {}
-                            );
-                            const intervalString =
-                                (schedule_interval > 1 ? `${schedule_interval} ` : '') +
-                                (schedule.RecurrenceType === 'Daily'
-                                    ? 'Day'
-                                    : schedule.RecurrenceType.slice(0, -2) +
-                                      (schedule_interval > 1 ? 's' : ''));
-                            Util.logger.warn(
-                                ` - scheduled automation '${
-                                    originalMetadataMap[key].name
-                                }' deployed Active: runs every ${intervalString} starting ${
-                                    schedule_StartDateTime.split('T').join(' ').split('.')[0]
-                                } ${schedule_timezoneString}`
-                            );
-                        } catch (ex) {
-                            Util.logger.error(
-                                `- Could not start scheduled automation '${originalMetadataMap[key].name}': ${ex.message}`
-                            );
-                        }
-                    }
-                } else {
-                    Util.logger.warn(
-                        ` - scheduled automation '${originalMetadataMap[key].name}' deployed Paused`
-                    );
                 }
-            }
-            if (metadataMap[key].startSource) {
-                metadataMap[key].schedule = metadataMap[key].startSource.schedule;
-
-                delete metadataMap[key].startSource;
-            }
-            if (metadataMap[key].schedule) {
-                metadataMap[key].schedule.typeId = metadataMap[key].schedule.scheduleTypeId;
-                delete metadataMap[key].schedule.scheduleTypeId;
-            }
-
-            // re-retrieve deployed items because the API does not return any info for them except the new id (api key)
-            try {
-                const { metadata } = await this.retrieve(null, null, null, key);
-                metadataMap[key] = Object.values(metadata)[0];
-                // postRetrieveTasks will be run automatically on this via super.saveResult
-            } catch (ex) {
-                throw new Error(
-                    `Could not get details for new ${this.definition.type} ${key} from server (${ex.message})`
+            } else {
+                Util.logger.info(
+                    Util.getGrayMsg(
+                        ` - scheduled automation '${originalMetadataMap[key].name}' deployed as Paused`
+                    )
                 );
             }
         }
+        if (metadataMap[key].startSource) {
+            metadataMap[key].schedule = metadataMap[key].startSource.schedule;
+
+            delete metadataMap[key].startSource;
+        }
+        if (metadataMap[key].schedule?.scheduleTypeId) {
+            metadataMap[key].schedule.typeId = metadataMap[key].schedule.scheduleTypeId;
+            delete metadataMap[key].schedule.scheduleTypeId;
+        }
     }
+
     /**
      * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
      *
@@ -884,6 +1055,27 @@ class Automation extends MetadataType {
             output += `* Pattern: ${json.fileTrigger.fileNamingPattern}\n`;
             output += `* Folder:  ${json.fileTrigger.folderLocationText}\n`;
         }
+        // add empty line to ensure the following notifications are rendered properly
+        output += '\n';
+        if (json.notifications?.length) {
+            output += `**Notifications:**\n\n`;
+            // ensure notifications are sorted by type regardless of how the API returns it
+            const notifications = {};
+            for (const n of json.notifications) {
+                notifications[n.type] =
+                    (Array.isArray(n.email) ? n.email.join(',') : n.email) +
+                    (n.message ? ` ("${n.message}")` : '');
+            }
+            if (notifications.Complete) {
+                output += `* Complete: ${notifications.Complete}\n`;
+            }
+            if (notifications.Error) {
+                output += `* Error: ${notifications.Error}\n`;
+            }
+        } else {
+            output += `**Notifications:** _none_\n\n`;
+        }
+
         // show table with automation steps
         if (tabled && tabled.length) {
             // add empty line to ensure the following table is rendered properly
diff --git a/lib/metadataTypes/definitions/Automation.definition.js b/lib/metadataTypes/definitions/Automation.definition.js
index 38e25d78..9f8560b4 100644
--- a/lib/metadataTypes/definitions/Automation.definition.js
+++ b/lib/metadataTypes/definitions/Automation.definition.js
@@ -46,7 +46,7 @@ module.exports = {
     createdNameField: 'createdByName',
     lastmodDateField: 'lastSavedDate',
     lastmodNameField: 'lastSavedByName',
-    restPagination: false,
+    restPagination: true,
     statusMapping: {
         AwaitingTrigger: 7,
         Building: 1,
@@ -329,7 +329,7 @@ module.exports = {
         legacyId: {
             isCreateable: false,
             isUpdateable: false,
-            retrieving: true,
+            retrieving: false,
             template: false,
         },
         lastSavedDate: {
@@ -368,6 +368,45 @@ module.exports = {
             retrieving: true,
             template: true,
         },
+        notifications: {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        'notifications[].email': {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        'notifications[].message': {
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        'notifications[].channelType': {
+            // always 'Account'
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: false,
+            template: false,
+        },
+        'notifications[].type': {
+            // custom shorthand for channelType
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
+        'notifications[].notificationType': {
+            // Error, Complete
+            isCreateable: true,
+            isUpdateable: true,
+            retrieving: true,
+            template: true,
+        },
         startSource: {
             skipValidation: true,
         },
@@ -425,6 +464,12 @@ module.exports = {
             retrieving: true,
             template: true,
         },
+        'schedule.statusId': {
+            isCreateable: false,
+            isUpdateable: false,
+            retrieving: false,
+            template: false,
+        },
         'schedule.timezoneId': {
             isCreateable: true,
             isUpdateable: true,
@@ -527,8 +572,8 @@ module.exports = {
         'steps[].annotation': {
             isCreateable: true,
             isUpdateable: true,
-            retrieving: true,
-            template: true,
+            retrieving: false,
+            template: false,
         },
         'steps[].id': {
             isCreateable: true,
diff --git a/lib/util/util.js b/lib/util/util.js
index 482cbd0c..4b60319a 100644
--- a/lib/util/util.js
+++ b/lib/util/util.js
@@ -85,7 +85,6 @@ const Util = {
      *
      * @param {string} mlName name of marketList
      * @param {TYPE.Mcdevrc} properties General configuration to be used in retrieve
-     * @returns {void} throws errors if problems were found
      */
     verifyMarketList(mlName, properties) {
         if (properties.marketList[mlName]) {
@@ -416,7 +415,7 @@ const Util = {
      * @param {string} cmd to be executed command
      * @param {string[]} [args] list of arguments
      * @param {boolean} [hideOutput] if true, output of command will be hidden from CLI
-     * @returns {string} output of command if hideOutput is true
+     * @returns {string|void} output of command if hideOutput is true
      */
     execSync(cmd, args, hideOutput) {
         args ||= [];


Test patch:
diff --git a/test/mockRoot/deploy/testInstance/testBU/automation/testExisting_automation.automation-meta.json b/test/mockRoot/deploy/testInstance/testBU/automation/testExisting_automation.automation-meta.json
index 47860e94..b901a271 100644
--- a/test/mockRoot/deploy/testInstance/testBU/automation/testExisting_automation.automation-meta.json
+++ b/test/mockRoot/deploy/testInstance/testBU/automation/testExisting_automation.automation-meta.json
@@ -42,5 +42,12 @@
             "name": ""
         }
     ],
+    "notifications": [
+        {
+            "email": ["error-updated@test.accenture.com"],
+            "message": "test updated",
+            "type": "Error"
+        }
+    ],
     "type": "scheduled"
 }
diff --git a/test/resources/9999999/automation/build-expected.json b/test/resources/9999999/automation/build-expected.json
index 30af1306..ecd94cb3 100644
--- a/test/resources/9999999/automation/build-expected.json
+++ b/test/resources/9999999/automation/build-expected.json
@@ -42,5 +42,17 @@
             "name": ""
         }
     ],
-    "type": "scheduled"
+    "type": "scheduled",
+    "notifications": [
+        {
+            "email": ["complete@test.accenture.com"],
+            "message": "",
+            "type": "Complete"
+        },
+        {
+            "email": ["error@test.accenture.com"],
+            "message": "test",
+            "type": "Error"
+        }
+    ]
 }
diff --git a/test/resources/9999999/automation/retrieve-testNew_automation-expected.md b/test/resources/9999999/automation/create-testNew_automation-expected.md
similarity index 95%
rename from test/resources/9999999/automation/retrieve-testNew_automation-expected.md
rename to test/resources/9999999/automation/create-testNew_automation-expected.md
index 354c1ee7..d596cc54 100644
--- a/test/resources/9999999/automation/retrieve-testNew_automation-expected.md
+++ b/test/resources/9999999/automation/create-testNew_automation-expected.md
@@ -15,6 +15,9 @@
 * Timezone:  W. Europe Standard Time
 * Recurrance: every  day for 1 times
 
+**Notifications:** _none_
+
+
 | Step 1<br>_<small>-</small>_ |
 | --- |
 | _1.1: dataExtract_<br>testExisting_dataExtract |
diff --git a/test/resources/9999999/automation/retrieve-expected.json b/test/resources/9999999/automation/retrieve-expected.json
index d9f83503..fa80b0f9 100644
--- a/test/resources/9999999/automation/retrieve-expected.json
+++ b/test/resources/9999999/automation/retrieve-expected.json
@@ -42,5 +42,17 @@
             "name": ""
         }
     ],
-    "type": "scheduled"
+    "type": "scheduled",
+    "notifications": [
+        {
+            "email": ["complete@test.accenture.com"],
+            "message": "",
+            "type": "Complete"
+        },
+        {
+            "email": ["error@test.accenture.com"],
+            "message": "test",
+            "type": "Error"
+        }
+    ]
 }
diff --git a/test/resources/9999999/automation/retrieve-testExisting_automation-expected.md b/test/resources/9999999/automation/retrieve-testExisting_automation-expected.md
index ca594881..22bd4c50 100644
--- a/test/resources/9999999/automation/retrieve-testExisting_automation-expected.md
+++ b/test/resources/9999999/automation/retrieve-testExisting_automation-expected.md
@@ -15,6 +15,11 @@
 * Timezone:  W. Europe Standard Time
 * Recurrance: every  day for 1 times
 
+**Notifications:**
+
+* Complete: complete@test.accenture.com
+* Error: error@test.accenture.com ("test")
+
 | Step 1<br>_<small>-</small>_ |
 | --- |
 | _1.1: dataExtract_<br>testExisting_dataExtract |
diff --git a/test/resources/9999999/automation/template-expected.json b/test/resources/9999999/automation/template-expected.json
index 33b49be6..038ecc7f 100644
--- a/test/resources/9999999/automation/template-expected.json
+++ b/test/resources/9999999/automation/template-expected.json
@@ -42,5 +42,17 @@
             "name": ""
         }
     ],
-    "type": "scheduled"
+    "type": "scheduled",
+    "notifications": [
+        {
+            "email": ["complete@test.accenture.com"],
+            "message": "",
+            "type": "Complete"
+        },
+        {
+            "email": ["error@test.accenture.com"],
+            "message": "test",
+            "type": "Error"
+        }
+    ]
 }
diff --git a/test/resources/9999999/automation/update-expected.json b/test/resources/9999999/automation/update-expected.json
index d9f83503..47860e94 100644
--- a/test/resources/9999999/automation/update-expected.json
+++ b/test/resources/9999999/automation/update-expected.json
@@ -1,5 +1,5 @@
 {
-    "description": "bla bla",
+    "description": "updated on deploy",
     "key": "testExisting_automation",
     "name": "testExisting_automation",
     "r__folder_Path": "my automations",
diff --git a/test/resources/9999999/automation/update-testExisting_automation-expected.md b/test/resources/9999999/automation/update-testExisting_automation-expected.md
new file mode 100644
index 00000000..681c2a69
--- /dev/null
+++ b/test/resources/9999999/automation/update-testExisting_automation-expected.md
@@ -0,0 +1,28 @@
+## testExisting_automation
+
+**Description:** updated on deploy
+
+**Folder:** my automations/
+
+**Started by:** Schedule
+
+**Status:** PausedSchedule
+
+**Schedule:**
+
+* Start: 2022-07-30 00:00:00 +01:00
+* End: 2022-07-30 00:00:00 +01:00
+* Timezone:  W. Europe Standard Time
+* Recurrance: every  day for 1 times
+
+**Notifications:** _none_
+
+
+| Step 1<br>_<small>-</small>_ |
+| --- |
+| _1.1: dataExtract_<br>testExisting_dataExtract |
+| _1.2: emailSend_<br>testExisting_emailSend |
+| _1.3: fileTransfer_<br>testExisting_fileTransfer |
+| _1.4: importFile_<br>testExisting_importFile |
+| _1.5: query_<br>testExisting_query |
+| _1.6: script_<br>testExisting_script |
diff --git a/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/patch-response.json b/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/patch-response.json
index edd818bd..1c0dc1b0 100644
--- a/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/patch-response.json
+++ b/test/resources/9999999/automation/v1/automations/08afb0e2-b00a-4c88-ad2e-1f7f8788c560/patch-response.json
@@ -1,4 +1,5 @@
 {
+    "legacyId": "RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow",
     "name": "testExisting_automation",
     "description": "updated on deploy",
     "key": "testExisting_automation",
diff --git a/test/resources/9999999/automation/v1/automations/post-response.json b/test/resources/9999999/automation/v1/automations/post-response.json
index 9053199c..6f82ef7c 100644
--- a/test/resources/9999999/automation/v1/automations/post-response.json
+++ b/test/resources/9999999/automation/v1/automations/post-response.json
@@ -1,4 +1,5 @@
 {
+    "legacyId": "NewRkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow",
     "name": "testNew_automation",
     "description": "created on deploy",
     "key": "testNew_automation",
diff --git a/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/get-response.json b/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/get-response.json
new file mode 100644
index 00000000..02c3b73d
--- /dev/null
+++ b/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/get-response.json
@@ -0,0 +1,21 @@
+{
+    "programId": "ZGQ4N2M2ZjEtYjJlZi00MDljLTg3MDctYzYwNWQ0MzdiYWI4OjI1OjA",
+    "workers": [
+        {
+            "programId": "ZGQ4N2M2ZjEtYjJlZi00MDljLTg3MDctYzYwNWQ0MzdiYWI4OjI1OjA",
+            "id": "YjA2NmVlODEtODllOS00NTE4LTkwYzYtMTY4Mzg2ZTk3MGE4OjEyOjA",
+            "notificationType": "Complete",
+            "definition": "complete@test.accenture.com",
+            "body": "",
+            "channelType": "Account"
+        },
+        {
+            "programId": "ZGQ4N2M2ZjEtYjJlZi00MDljLTg3MDctYzYwNWQ0MzdiYWI4OjI1OjA",
+            "id": "NGIzZGQ5N2EtMDI0ZS00MzFiLTg5ZWYtNTdjYTI4ODVjMjQ0OjEyOjA",
+            "notificationType": "Error",
+            "definition": "error@test.accenture.com",
+            "body": "test",
+            "channelType": "Account"
+        }
+    ]
+}
diff --git a/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/post-response.json b/test/resources/9999999/legacy/v1/beta/automations/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow/post-response.json
new file mode 100644
index 00000000..e69de29b
diff --git a/test/resources/9999999/legacy/v1/beta/bulk/automations/automation/definition/get-response.json b/test/resources/9999999/legacy/v1/beta/bulk/automations/automation/definition/get-response.json
new file mode 100644
index 00000000..bbf38898
--- /dev/null
+++ b/test/resources/9999999/legacy/v1/beta/bulk/automations/automation/definition/get-response.json
@@ -0,0 +1,30 @@
+{
+    "startIndex": 0,
+    "itemsPerPage": 50,
+    "totalResults": 1,
+    "entry": [
+        {
+            "id": "RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoyNTow",
+            "key": "testExisting_automation",
+            "createdDate": "2022-01-12T08:41:35.773Z",
+            "name": "testExisting_automation",
+            "description": "bla bla",
+            "clientId": 9999999,
+            "status": "Building",
+            "createdBy": {
+                "id": "NzE3MzUzNDA1OjQ6MA",
+                "name": "Tom Tester",
+                "email": "tom.tester@accenture.com"
+            },
+            "modifiedDate": "2022-11-11T16:42:36.513Z",
+            "modifiedBy": {
+                "id": "NzE3MzUzNDA1OjQ6MA",
+                "name": "Tom Tester",
+                "email": "tom.tester@accenture.com"
+            },
+            "isPlatformObject": false,
+            "notifications": "https://mcxxxxx.rest.marketingcloudapis.com/legacy/v1/beta/hub/notifications/RkpOcE9qSVh2VUdnYTVJbWFfWW14dzoxMjow",
+            "automationType": "scheduled"
+        }
+    ]
+}
diff --git a/test/type.automation.test.js b/test/type.automation.test.js
index e78c4514..5f9dd60b 100644
--- a/test/type.automation.test.js
+++ b/test/type.automation.test.js
@@ -50,7 +50,7 @@ describe('type: automation', () => {
 
             assert.equal(
                 testUtils.getAPIHistoryLength(),
-                12,
+                14,
                 'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
             );
             return;
@@ -91,7 +91,7 @@ describe('type: automation', () => {
                     testUtils.getExpectedFile(
                         '9999999',
                         'automation',
-                        'retrieve-testExisting_automation',
+                        'update-testExisting_automation',
                         'md'
                     )
                 )
@@ -103,7 +103,7 @@ describe('type: automation', () => {
                     testUtils.getExpectedFile(
                         '9999999',
                         'automation',
-                        'retrieve-testNew_automation',
+                        'create-testNew_automation',
                         'md'
                     )
                 )
@@ -111,7 +111,7 @@ describe('type: automation', () => {
 
             assert.equal(
                 testUtils.getAPIHistoryLength(),
-                17,
+                15,
                 'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
             );
             return;
@@ -163,7 +163,7 @@ describe('type: automation', () => {
             );
             assert.equal(
                 testUtils.getAPIHistoryLength(),
-                12,
+                14,
                 'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
             );
             return;
@@ -210,7 +210,7 @@ describe('type: automation', () => {
             );
             assert.equal(
                 testUtils.getAPIHistoryLength(),
-                12,
+                14,
                 'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
             );
             return;