├── LICENSE ├── README.md ├── Services.js ├── app.js ├── app.yaml ├── package.json └── settings.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bogdan Ripa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UiPath Orchestrator Smart Scheduler 2 | Node.js app that will manage schedules to: 3 | * Automatically run a process when a queue item is added 4 | * Start a process when another one has finished. The initial process's output becomes the input for your new process 5 | * Automatically retry processes that fail 6 | * Link queue items, automatically creating new queue items when other queue item(s) are marked as completed. Completed queue items output (in aggregate) becomes the input for the new queue item 7 | 8 | See the [settings.json](settings.json) file for details on how to set those up. 9 | 10 | ## Installation / set-up instructions 11 | 12 | **! The easiest way to deply this is using Google Cloud's App Engine: https://cloud.google.com/appengine/** 13 | 14 | 1. In Google Cloud / App Engine, create a new empty project and connect to it 15 | 2. git clone this repo and go in it's folder 16 | 3. run "npm install" 17 | 4. edit the settings.json file and 18 | 1. add your services. A service consists of a queue name, a process name, an environment name, and the maximum number of robots to run in paralel. This is how you start a process when queue items are added. 19 | 2. add process links. The input represent the process you want to monitor for completion. the output represents the process(es) you want to start when the previous one finishes succesfully. 20 | 3. add process retries. This is how you retry a process on fail. Warning! When a process is retried, its input arguments are lost (at least for now) 21 | 4. add queue links. The input represent the queue(s) that you want to monitor for completed items. The output represents the queue(s) where you want new queue items to be added. Queue items are matched by reference. 22 | 5. also in settings.json, update the Orchestrator connectivity details and the webhooks secret key 23 | 6. run "gcloud app deploy" to deploy your service 24 | 7. In Orchestrator (2018.4 or newer), set-up 4 webhooks that will point to your endpoints: 25 | 26 | * http://PROJECTNAME.appspot.com/webhooks/jobs/created subscribed to job.created 27 | * http://PROJECTNAME.appspot.com/webhooks/jobs/finished subscribed to job.completed, job.faulted, job.stopped 28 | * http://PROJECTNAME.appspot.com/webhooks/queues/items/created subscribed to queueItems.added 29 | * http://PROJECTNAME.appspot.com/webhooks/queues/items/completed subscribed to queueItem.transactionCompleted 30 | 31 | 8. In Orchestrator, make sure that your API user (as defined in settings.json) has the right to view processes, queues, transactions and jobs, and to create transaction and jobs 32 | -------------------------------------------------------------------------------- /Services.js: -------------------------------------------------------------------------------- 1 | var Orchestrator = require('uipath-orchestrator'); 2 | 3 | function Services(settings) { 4 | this.settings = settings; 5 | this.settings.queues = {}; 6 | this.settings.processes = {}; 7 | this.orchestrator = new Orchestrator(settings.connection); 8 | } 9 | 10 | function odataEscape(str) { 11 | return str.replace(/'/g, "''"); 12 | } 13 | 14 | Services.prototype.getJobDetails = function(jobId) { 15 | return new Promise(function (fulfill, reject){ 16 | this.orchestrator.v2.odata.getJob(jobId, {}, function(err, data) { 17 | if (err) { 18 | reject(err); 19 | } else { 20 | fulfill(data); 21 | } 22 | }); 23 | }.bind(this)); 24 | } 25 | 26 | // updates the service object with the number of jobs currently running 27 | Services.prototype.getRunningJobs = function(service) { 28 | return new Promise(function (fulfill, reject){ 29 | this.orchestrator.v2.odata.getJobs({"$filter": "ReleaseName eq '" + odataEscape(service.processName) + "_" + odataEscape(service.environmentName) + "' and (State eq 'Pending' or State eq 'Running')", "$top": 0, "$count": "true"}, function(err, data) { 30 | if (err) { 31 | reject(err); 32 | } else { 33 | try { 34 | service.count = data["@odata.count"]; 35 | fulfill(); 36 | } catch(err) { 37 | reject("Malformed response: Cannot get jobs count"); 38 | } 39 | } 40 | }); 41 | }.bind(this)); 42 | } 43 | 44 | // updates the service object with the process key, needed for further API calls 45 | Services.prototype.getProcessKey = function(processName, environmentName) { 46 | return new Promise(function (fulfill, reject){ 47 | if (this.settings.processes[processName + "_" + environmentName]) { 48 | fulfill(); 49 | return; 50 | } 51 | this.orchestrator.v2.odata.getReleases({"$filter": "ProcessKey eq '" + odataEscape(processName) + "' and EnvironmentName eq '" + odataEscape(environmentName) + "'"}, function(err, data) { 52 | if (err) { 53 | reject(err); 54 | } else { 55 | try { 56 | this.settings.processes[processName + "_" + environmentName] = data.value[0].Key; 57 | fulfill(); 58 | } catch(err) { 59 | reject("Malformed response: Cannot get process key for " + processName +" on " + environmentName + ": " + err); 60 | } 61 | } 62 | }.bind(this)); 63 | }.bind(this)); 64 | } 65 | 66 | // updates the service object with the queue Id 67 | Services.prototype.getQueueId = function(queueName) { 68 | return new Promise(function (fulfill, reject){ 69 | this.orchestrator.v2.odata.getQueueDefinitions({"$filter": "Name eq '" + odataEscape(queueName) + "'"}, function(err, data) { 70 | if (err) { 71 | reject(err); 72 | } else { 73 | try { 74 | this.settings.queues[queueName] = data.value[0].Id; 75 | fulfill(); 76 | } catch(err) { 77 | reject("Malformed response: Cannot get queue dewfinition for " + queueName); 78 | } 79 | } 80 | }.bind(this)); 81 | }.bind(this)); 82 | } 83 | 84 | // gets number of running jobs and process keys for all services 85 | Services.prototype.getProcessDetails = function() { 86 | return new Promise(function (fulfill, reject){ 87 | var servicesPromises = []; 88 | this.settings.services.forEach(function(service) { 89 | servicesPromises.push(this.getRunningJobs(service)); 90 | if (!this.settings.processes[service.processName + "_" + service.environmentName]) { 91 | servicesPromises.push(this.getProcessKey(service.processName, service.environmentName)); 92 | } 93 | if (!this.settings.queues[service.queueName]) { 94 | servicesPromises.push(this.getQueueId(service.queueName)); 95 | } 96 | }.bind(this)); 97 | 98 | this.settings.processRetries.forEach(function(process) { 99 | servicesPromises.push(this.getProcessKey(process.processName, process.environmentName)); 100 | }.bind(this)); 101 | 102 | this.settings.processLinks.forEach(function(processLink) { 103 | processLink.output.forEach(function(linkOutput) { 104 | servicesPromises.push(this.getProcessKey(linkOutput.processName, linkOutput.environmentName)); 105 | }.bind(this)); 106 | servicesPromises.push(this.getProcessKey(processLink.input.processName, processLink.input.environmentName)); 107 | }.bind(this)); 108 | 109 | Promise.all(servicesPromises) 110 | .then(function() { 111 | console.log("Got all current running jobs details"); 112 | fulfill(); 113 | }) 114 | .catch(reject); 115 | }.bind(this)); 116 | } 117 | 118 | Services.prototype.getQueueDetails = function() { 119 | return new Promise(function (fulfill, reject){ 120 | var queuePromises = []; 121 | this.settings.queueLinks.forEach(function(queueLink) { 122 | queueLink.input.forEach(function(queueName) { 123 | if (!this.settings.queues[queueName]) { 124 | queuePromises.push(this.getQueueId(queueName)); 125 | } 126 | }.bind(this)); 127 | 128 | queueLink.output.forEach(function(queueName) { 129 | if (!this.settings.queues[queueName]) { 130 | queuePromises.push(this.getQueueId(queueName)); 131 | } 132 | }.bind(this)); 133 | }.bind(this)); 134 | Promise.all(queuePromises) 135 | .then(function() { 136 | console.log("Got all current queue details"); 137 | fulfill(); 138 | }) 139 | .catch(reject); 140 | }.bind(this)); 141 | } 142 | 143 | // Start processing for a service. 144 | // This will try to strat as many jobs as possible to process asap the items in the corresponding queue 145 | Services.prototype.startProcessing = function(service) { 146 | return new Promise(function (fulfill, reject){ 147 | console.log(service.processName + ": " + service.count + " jobs running"); 148 | console.log(service.processName + ": " + service.maxRobots + " max jobs"); 149 | if (service.count >= service.maxRobots) { 150 | // no need to start new jobs, they are already running 151 | fulfill(); 152 | } 153 | // get the number of items to be processed in this queue 154 | this.orchestrator.v2.odata.getRetrieveQueuesProcessingStatus({"$filter": "QueueDefinitionName eq '" + service.queueName + "'"}, function(err, data) { 155 | if (err) { 156 | reject(err); 157 | } else { 158 | try { 159 | var itemsToProcess = data.value[0].ItemsToProcess; 160 | console.log(service.queueName + ": " + itemsToProcess + " items to process"); 161 | var newJobsCount = Math.min(itemsToProcess, service.maxRobots - service.count); 162 | console.log(service.processName + ": " + newJobsCount + " jobs to start"); 163 | if (newJobsCount > 0) { 164 | this.startJobForQueue(this.settings.queues[service.queueName], newJobsCount); 165 | } 166 | fulfill(); 167 | } catch(err) { 168 | reject("Malformed response: Cannot get Queue size"); 169 | } 170 | } 171 | }.bind(this)); 172 | fulfill(); 173 | }.bind(this)); 174 | } 175 | 176 | // starts processing jobs for all services 177 | Services.prototype.startProcessingJobs = function() { 178 | return new Promise(function (fulfill, reject){ 179 | var servicesPromises = []; 180 | this.settings.services.forEach(function(service) { 181 | servicesPromises.push(this.startProcessing(service)); 182 | }.bind(this)); 183 | Promise.all(servicesPromises) 184 | .then(function() { 185 | fulfill(); 186 | }) 187 | .catch(reject); 188 | }.bind(this)); 189 | } 190 | 191 | Services.prototype.startJob = function(jobName, environmentName, runs, inputArgs) { 192 | console.log("Starting " + jobName + " on " + environmentName + " " + runs + " time(s) with " + JSON.stringify(inputArgs)); 193 | this.getProcessKey(jobName, environmentName).then(function() { 194 | jobParams = { 195 | "startInfo": { 196 | "ReleaseKey": this.settings.processes[jobName + "_" + environmentName], 197 | "Strategy": "JobsCount", 198 | "JobsCount": runs, 199 | "Source": "Schedule", 200 | "InputArguments": JSON.stringify(inputArgs) 201 | } 202 | }; 203 | 204 | this.orchestrator.post("/odata/Jobs/UiPath.Server.Configuration.OData.StartJobs", jobParams, function(err, data){ 205 | if (err) { 206 | console.log(err); 207 | } 208 | }); 209 | }.bind(this)).catch(function(e) { 210 | console.log(e); 211 | }); 212 | }; 213 | 214 | // queues a number of jobs for a specific process corresponding to a queue id 215 | Services.prototype.startJobForQueue = function(queueId, runs) { 216 | var key = ''; 217 | var shouldRun = false; 218 | 219 | var service = this.settings.services.find(function(service) { 220 | return this.settings.queues[service.queueName] == queueId; 221 | }.bind(this)); 222 | 223 | if (service) { 224 | if (service.count + 1 > service.maxRobots) { 225 | console.log(service.queueName + ": max robots reached"); 226 | return; 227 | } 228 | this.startJob(service.processName, service.environmentName, runs, {}); 229 | } 230 | } 231 | 232 | Services.prototype.onJobFinished = function(job) { 233 | var jobName; 234 | if (!job) return; 235 | Object.keys(this.settings.processes).forEach(function(processName) { 236 | if (this.settings.processes[processName] == job.Release.Key) { 237 | jobName = processName; 238 | } 239 | }.bind(this)); 240 | 241 | var service = this.settings.services.find(function(service) { 242 | return jobName == service.processName + "_" + service.environmentName; 243 | }); 244 | if (service) { 245 | service.count--; 246 | console.log(jobName + ": is running " + service.count + " time(s)"); 247 | } 248 | 249 | var processRetry = this.settings.processRetries.find(function(process) { 250 | return jobName == process.processName + "_" + process.environmentName; 251 | }); 252 | 253 | switch (job.State) { 254 | case "Successful": 255 | 256 | if (processRetry) { 257 | processRetry.failCount = 0; 258 | } 259 | 260 | var processLink = this.settings.processLinks.find(function(processLink) { 261 | return jobName == processLink.input.processName + "_" + processLink.input.environmentName; 262 | }); 263 | if (processLink) { 264 | processLink.output.forEach(function(outputLink) { 265 | this.startJob(outputLink.processName, outputLink.environmentName, 1, job.OutputArguments); 266 | }.bind(this)); 267 | } 268 | break; 269 | case "Faulted": 270 | if (processRetry) { 271 | if (!processRetry.failCount) { 272 | processRetry.failCount = 0; 273 | } 274 | processRetry.failCount++; 275 | if (processRetry.failCount <= processRetry.retries) { 276 | this.getJobDetails(job.Id).then(function(job) { 277 | console.log("Process execution failed for " + processRetry.processName + " on " + processRetry.environmentName + ". Retrying..."); 278 | this.startJob(processRetry.processName, processRetry.environmentName, 1, JSON.parse(job.InputArguments)); 279 | }.bind(this)); 280 | } else { 281 | console.log("Retry count exceded for " + processRetry.processName + " on " + processRetry.environmentName); 282 | } 283 | } 284 | break; 285 | } 286 | } 287 | 288 | Services.prototype.onJobCreated = function(jobName) { 289 | var service = this.settings.services.find(function(service) { 290 | return jobName == service.processName + "_" + service.environmentName; 291 | }); 292 | if (service) { 293 | service.count++; 294 | console.log(jobName + ": is running " + service.count + " time(s)"); 295 | } 296 | } 297 | 298 | function arrayContainsArray (superset, subset) { 299 | if (0 === subset.length) { 300 | return false; 301 | } 302 | return subset.every(function (value) { 303 | return (superset.indexOf(value) >= 0); 304 | }); 305 | } 306 | 307 | Services.prototype.checkQueueLinks = function(queue) { 308 | var partOfALink = false; 309 | var queueLinks = []; 310 | this.settings.queueLinks.forEach(function(queueLink) { 311 | queueLink.input.forEach(function(queueName) { 312 | if (queue.QueueDefinitionId == this.settings.queues[queueName]) { 313 | queueLinks.push(queueLink); 314 | partOfALink = true; 315 | } 316 | }.bind(this)); 317 | }.bind(this)); 318 | 319 | if (partOfALink) { 320 | this.orchestrator.v2.odata.getQueueItems({"$filter": "Reference eq '" + queue.Reference + "' and Status eq 'Successful'"}, function(err, data) { 321 | if (err) { 322 | console.log(err); 323 | } else { 324 | try { 325 | var queueIDs = {}; 326 | var queueNames = []; 327 | var queueOutput = {}; 328 | data.value.forEach(function(queueItem) { 329 | if (!queueOutput[queueItem.QueueDefinitionId]) { 330 | queueIDs[queueItem.QueueDefinitionId] = true; 331 | queueOutput = Object.assign(queueOutput, queueItem.Output); 332 | } 333 | }.bind(this)); 334 | 335 | Object.keys(this.settings.queues).forEach(function(queueName) { 336 | if (queueIDs[this.settings.queues[queueName]]) { 337 | queueNames.push(queueName); 338 | } 339 | }.bind(this)); 340 | 341 | queueLinks.forEach(function(queueLink) { 342 | if (arrayContainsArray(queueNames, queueLink.input)) { 343 | // all items were processed, create new queue item 344 | 345 | queueLink.output.forEach(function(queueName) { 346 | var newQueueItem = { 347 | "itemData": { 348 | "Name": queueName, 349 | "SpecificContent": queueOutput, 350 | "Reference": queue.Reference 351 | } 352 | }; 353 | this.orchestrator.v2.odata.postAddQueueItem(newQueueItem, function(err, data) { 354 | if (err) { 355 | if (data && data.message) { 356 | console.log(data.message); 357 | return; 358 | } 359 | console.log(err); 360 | return; 361 | } 362 | console.log("Queue Link matched, created new queue item"); 363 | }); 364 | }.bind(this)); 365 | } 366 | }.bind(this)); 367 | 368 | } catch(err) { 369 | console.log("Malformed response: Cannot get queue items by reference: " + err); 370 | } 371 | } 372 | }.bind(this)); 373 | 374 | } 375 | } 376 | 377 | module.exports = Services; 378 | 379 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const Services = require('./Services.js'); 4 | const { createHmac } = require('crypto'); 5 | const md5 = require('md5'); 6 | const app = express(); 7 | 8 | app.use(bodyParser.json({ 9 | verify: function(req, res, buf, encoding) { 10 | req.buffer = buf; 11 | } 12 | })); 13 | 14 | app.use(bodyParser.json()); 15 | app.use(bodyParser.urlencoded({ extended: true })); 16 | 17 | //var settingsFile = process.argv[1].replace(/\/[^\/]+$/, "/settings.json"); 18 | var settings = require('./settings.json'); 19 | var services = new Services(settings); 20 | 21 | // in case a webhook api call is missed, or if queue items become available to be processed (ex: the ones that were delayed), the jobs count cash is refreshed, and new jobs are started as needed 22 | function refresh() { 23 | return new Promise(function (fulfill, reject){ 24 | services.getQueueDetails() 25 | .then(services.getProcessDetails.bind(services)) 26 | .then(services.startProcessingJobs.bind(services)) 27 | .then(fulfill) 28 | .catch(reject); 29 | 30 | }); 31 | } 32 | 33 | function init() { 34 | // get the number of jobs currently running or pending for all services 35 | services.getProcessDetails() 36 | .then(services.getQueueDetails.bind(services)) 37 | .then(function() { 38 | const server = app.listen(process.env.PORT || 8080, "0.0.0.0", function() { 39 | const port = server.address().port; 40 | console.log(`Listening on port ${port}!`) 41 | }); 42 | services.startProcessingJobs(); 43 | setInterval(refresh, settings.refreshInterval*1000); 44 | }) 45 | .catch(function(err) { 46 | console.log(err); 47 | }); 48 | } 49 | 50 | // checks signature to authenticate the caller (UiPath) 51 | function checkSecretKey(signature, buffer) { 52 | return createHmac('sha256', settings.secretKey).update(buffer).digest('base64') === signature; 53 | } 54 | 55 | // checks signature to authenticate the caller (UiPath) 56 | function checkBasicSecretKey(signature) { 57 | return md5(settings.secretKey) == signature; 58 | } 59 | 60 | app.get('/', function(req, res) { 61 | res.send('Hello World!'); 62 | }); 63 | 64 | app.post('/webhooks/jobs/created', function(req, res) { 65 | if (!checkSecretKey(req.headers['x-uipath-signature'], req.buffer)) { 66 | console.log("Wrong signature!"); 67 | res.status(401).send(); 68 | return; 69 | } 70 | 71 | req.body.Jobs.forEach(function(job) { 72 | services.onJobCreated(job.ReleaseName); 73 | }); 74 | res.send(); 75 | }); 76 | 77 | app.post('/webhooks/jobs/finished', function(req, res) { 78 | if (!checkSecretKey(req.headers['x-uipath-signature'], req.buffer)) { 79 | console.log("Wrong signature!"); 80 | res.status(401).send(); 81 | return; 82 | } 83 | services.onJobFinished(req.body.Job); 84 | 85 | res.send(); 86 | }); 87 | 88 | app.post('/webhooks/queues/items/created', function(req, res) { 89 | if (!checkSecretKey(req.headers['x-uipath-signature'], req.buffer)) { 90 | console.log("Wrong signature!"); 91 | res.status(401).send(); 92 | return; 93 | } 94 | 95 | req.body.QueueItems.forEach(function(queueItem) { 96 | services.startJobForQueue(queueItem.QueueDefinitionId, 1); 97 | }); 98 | 99 | res.send(); 100 | }); 101 | 102 | app.post('/webhooks/queues/items/completed', function(req, res) { 103 | if (!checkSecretKey(req.headers['x-uipath-signature'], req.buffer)) { 104 | console.log("Wrong signature!"); 105 | res.status(401).send(); 106 | return; 107 | } 108 | 109 | services.checkQueueLinks(req.body.QueueItem); 110 | 111 | res.send(); 112 | }); 113 | 114 | app.post('/webhooks/jobs/start', function(req, res) { 115 | if (!checkBasicSecretKey(req.body.secret)) { 116 | console.log("Wrong signature!"); 117 | res.status(401).send(); 118 | return; 119 | } 120 | services.startJob(req.body.processName, req.body.envName, 1, req.body.processArgs); 121 | res.status(200).send("Request sent"); 122 | return; 123 | }); 124 | 125 | app.all('*', function(req, res) { 126 | console.log(req.method + " " + req.originalUrl); 127 | console.log(req.body); 128 | console.log(req.headers); 129 | res.status(404).send('Hmm... are you trying to hack me?'); 130 | }); 131 | 132 | init(); 133 | -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2018, Google LLC. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | # 14 | # [START runtime] 15 | runtime: nodejs10 16 | # [END runtime] 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UiPath-Run-Queues", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "e2e": "repo-tools test deploy", 9 | "test": "repo-tools test app", 10 | "cover": "nyc --cache npm test; nyc report --reporter=html" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/bogdanripa/UiPath-Run-Queues.git" 15 | }, 16 | "keywords": [ 17 | "UiPath", 18 | "run", 19 | "queues" 20 | ], 21 | "author": "Bogdan Ripa", 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/bogdanripa/UiPath-Run-Queues/issues" 25 | }, 26 | "homepage": "https://github.com/bogdanripa/UiPath-Run-Queues", 27 | "dependencies": { 28 | "body-parser": "^1.18.2", 29 | "express": "^4.16.3", 30 | "json-stringify-safe": "^5.0.1", 31 | "md5": "^2.2.1", 32 | "uipath-orchestrator": "^1.0.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": [ 3 | { 4 | "queueName": "Leads", 5 | "processName": "processLeads", 6 | "environmentName": "all", 7 | "maxRobots": 2 8 | }, 9 | { 10 | "queueName": "KYC", 11 | "processName": "processKYC", 12 | "environmentName": "all", 13 | "maxRobots": 4 14 | } 15 | ], 16 | "queueLinks": [ 17 | { 18 | "input": [ 19 | "Leads" 20 | ], 21 | "output": [ 22 | "Prospects", 23 | "KYC" 24 | ] 25 | }, 26 | { 27 | "input": [ 28 | "Prospects", 29 | "KYC" 30 | ], 31 | "output": [ 32 | "Customers" 33 | ] 34 | } 35 | ], 36 | "processLinks": [ 37 | { 38 | "input": { 39 | "processName": "Step1", 40 | "environmentName": "all" 41 | }, 42 | "output": [ 43 | { 44 | "processName": "Step2", 45 | "environmentName": "all" 46 | } 47 | ] 48 | } 49 | ], 50 | "processRetries": [ 51 | { 52 | "processName": "PayInvoice", 53 | "environmentName": "all", 54 | "retries": 3 55 | } 56 | ], 57 | "connection": { 58 | "tenancyName": "Default", 59 | "usernameOrEmailAddress": "api", 60 | "password": "*******", 61 | "hostname": "platform.uipath.com", 62 | "isSecure": true, 63 | "port": 443, 64 | "invalidCertificate": false, 65 | "connectionPool": 5 66 | }, 67 | "refreshInterval": 3600, 68 | "secretKey": "YOUR_ORCHESTRATOR_WEBHOOKS_SECRET_KEY_HERE" 69 | } 70 | --------------------------------------------------------------------------------