workato-dev-api 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1 @@
1
+ WORKATO_API_TOKEN=your_token_here
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # workato-dev-api
2
+
3
+ A zero-dependency CLI for the [Workato Developer API](https://docs.workato.com/workato-api.html). Read and edit recipes, connections, data tables, projects, folders, and jobs from your terminal.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install -g workato-dev-api
9
+ ```
10
+
11
+ Or use without installing:
12
+
13
+ ```sh
14
+ npx workato-dev-api <command>
15
+ ```
16
+
17
+ ## Authentication
18
+
19
+ Set `WORKATO_API_TOKEN` in a `.env` file. The CLI checks these locations in order, with **later files winning**:
20
+
21
+ 1. `<package-dir>/.env` — lowest priority (rarely used)
22
+ 2. `~/.env` — your home directory default
23
+ 3. `./.env` (cwd) — **highest priority**, project-specific override
24
+
25
+ ```sh
26
+ # .env
27
+ WORKATO_API_TOKEN=your_token_here
28
+ ```
29
+
30
+ You can also export it directly in your shell environment.
31
+
32
+ ## Commands
33
+
34
+ ### Read
35
+
36
+ | Command | Description |
37
+ |---|---|
38
+ | `workato get <recipe_id>` | Fetch recipe code JSON → saved to `recipe_<id>_code.json` |
39
+ | `workato list-recipes` | List recipes. Filters: `--folder <id>`, `--project <id>`, `--page <n>` |
40
+ | `workato list-projects` | List all projects |
41
+ | `workato list-folders` | List folders. Filter: `--parent <id>` |
42
+ | `workato list-connections` | List connections. Filter: `--folder <id>` |
43
+ | `workato list-data-tables` | List data tables. Filter: `--project <id>` |
44
+ | `workato get-data-table <id>` | Fetch data table schema and details |
45
+ | `workato get-jobs <recipe_id>` | List recent jobs. Filters: `--limit <n>`, `--status <status>` |
46
+ | `workato get-job <recipe_id> <job_id>` | Fetch a single job |
47
+
48
+ ### Write
49
+
50
+ | Command | Description |
51
+ |---|---|
52
+ | `workato create "<name>" <code.json>` | Create a recipe from a full code JSON file |
53
+ | `workato create-api-trigger "<name>"` | Create a recipe with a bare API Platform trigger |
54
+ | `workato update-step <recipe_id> <step_as_id> <patch.json>` | Deep-merge a patch into one step (by `as` ID) |
55
+ | `workato put-code <recipe_id> <code.json>` | Replace an entire recipe's code |
56
+ | `workato start <recipe_id>` | Start a recipe |
57
+ | `workato stop <recipe_id>` | Stop a recipe |
58
+ | `workato delete <recipe_id>` | Delete a recipe |
59
+
60
+ ## Examples
61
+
62
+ ```sh
63
+ # Fetch recipe code and save to file
64
+ workato get 167603
65
+
66
+ # List all recipes in a project
67
+ workato list-recipes --project 14318
68
+
69
+ # List jobs that failed
70
+ workato get-jobs 167603 --limit 20 --status failed
71
+
72
+ # Start a recipe
73
+ workato start 167603
74
+
75
+ # Patch a single step's input fields
76
+ cat > patch.json <<'EOF'
77
+ {
78
+ "input": {
79
+ "language": "fr"
80
+ }
81
+ }
82
+ EOF
83
+ workato update-step 167603 5df21cfd patch.json
84
+
85
+ # Replace entire recipe code
86
+ workato put-code 167603 recipe_167603_code.json
87
+ ```
88
+
89
+ ## Recipe code structure
90
+
91
+ A recipe's code is a JSON object. The top-level object is the **trigger** step; action steps live in `code.block[]`. Each step has a unique `as` field (8-char hex) used for cross-step wiring (datapills).
92
+
93
+ `workato get <id>` saves the code to `recipe_<id>_code.json` so you can inspect and edit it before pushing back with `put-code`.
94
+
95
+ ## Development
96
+
97
+ ```sh
98
+ git clone ...
99
+ cd workato-dev-api
100
+ cp .env.example .env # add your token
101
+ npm test # runs 88 unit tests, no network required
102
+ ```
103
+
104
+ Tests use Node's built-in `node:test` runner — no extra dependencies.
package/cli.js ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const {
7
+ loadEnv,
8
+ cmdGet, cmdListRecipes, cmdListProjects, cmdListFolders,
9
+ cmdListConnections, cmdListDataTables, cmdGetDataTable,
10
+ cmdGetJobs, cmdGetJob,
11
+ cmdCreate, cmdCreateApiTrigger, cmdUpdateStep, cmdPutCode,
12
+ cmdStart, cmdStop, cmdDelete,
13
+ } = require('./lib');
14
+
15
+ // Load order — last one wins, so highest-priority sources go last:
16
+ // package dir → home dir → cwd (cwd always wins)
17
+ loadEnv(path.join(__dirname, '.env'));
18
+ loadEnv(path.join(os.homedir(), '.env'));
19
+ loadEnv(path.join(process.cwd(), '.env'));
20
+
21
+ if (!process.env.WORKATO_API_TOKEN) {
22
+ console.error('Error: WORKATO_API_TOKEN not set.\nCreate a .env file in your current directory with:\n WORKATO_API_TOKEN=your_token_here');
23
+ process.exit(1);
24
+ }
25
+
26
+ // Parse argv: separate --flag value pairs from positional args
27
+ function parseArgs(argv) {
28
+ const positional = [];
29
+ const flags = {};
30
+ for (let i = 0; i < argv.length; i++) {
31
+ if (argv[i].startsWith('--')) {
32
+ const key = argv[i].slice(2);
33
+ // treat next token as value unless it's also a flag or missing
34
+ const next = argv[i + 1];
35
+ if (next !== undefined && !next.startsWith('--')) {
36
+ flags[key] = next;
37
+ i++;
38
+ } else {
39
+ flags[key] = true;
40
+ }
41
+ } else {
42
+ positional.push(argv[i]);
43
+ }
44
+ }
45
+ return { positional, flags };
46
+ }
47
+
48
+ function usage() {
49
+ console.error(`
50
+ workato <command> [options]
51
+
52
+ Read commands:
53
+ get <recipe_id> Fetch recipe code → recipe_<id>_code.json
54
+ list-recipes [--folder <id>] [--project <id>] [--page <n>]
55
+ list-projects
56
+ list-folders [--parent <id>]
57
+ list-connections [--folder <id>]
58
+ list-data-tables [--project <id>]
59
+ get-data-table <id> Fetch data table schema/details
60
+ get-jobs <recipe_id> [--limit <n>] [--status <status>]
61
+ get-job <recipe_id> <job_id>
62
+
63
+ Write commands:
64
+ create "<name>" <code.json> Create recipe from full code JSON
65
+ create-api-trigger "<name>" Create recipe with a bare API Platform trigger
66
+ update-step <recipe_id> <step_as_id> <patch.json> Deep-merge patch into a step
67
+ put-code <recipe_id> <code.json> Replace entire recipe code
68
+ start <recipe_id> Start a recipe
69
+ stop <recipe_id> Stop a recipe
70
+ delete <recipe_id> Delete a recipe
71
+
72
+ Environment:
73
+ WORKATO_API_TOKEN Required. Set in a .env file in your current directory.
74
+ `.trim());
75
+ process.exit(1);
76
+ }
77
+
78
+ const [,, cmd, ...rawArgs] = process.argv;
79
+ const { positional: args, flags } = parseArgs(rawArgs);
80
+
81
+ (async () => {
82
+ try {
83
+ switch (cmd) {
84
+ // ── Read ──────────────────────────────────────────────────────────────
85
+ case 'get':
86
+ if (!args[0]) usage();
87
+ await cmdGet(args[0]);
88
+ break;
89
+
90
+ case 'list-recipes':
91
+ await cmdListRecipes({
92
+ folder_id: flags.folder,
93
+ project_id: flags.project,
94
+ page: flags.page,
95
+ });
96
+ break;
97
+
98
+ case 'list-projects':
99
+ await cmdListProjects();
100
+ break;
101
+
102
+ case 'list-folders':
103
+ await cmdListFolders({ parent_id: flags.parent });
104
+ break;
105
+
106
+ case 'list-connections':
107
+ await cmdListConnections({ folder_id: flags.folder });
108
+ break;
109
+
110
+ case 'list-data-tables':
111
+ await cmdListDataTables({ project_id: flags.project });
112
+ break;
113
+
114
+ case 'get-data-table':
115
+ if (!args[0]) usage();
116
+ await cmdGetDataTable(args[0]);
117
+ break;
118
+
119
+ case 'get-jobs':
120
+ if (!args[0]) usage();
121
+ await cmdGetJobs(args[0], { limit: flags.limit, status: flags.status });
122
+ break;
123
+
124
+ case 'get-job':
125
+ if (!args[0] || !args[1]) usage();
126
+ await cmdGetJob(args[0], args[1]);
127
+ break;
128
+
129
+ // ── Write ─────────────────────────────────────────────────────────────
130
+ case 'create':
131
+ if (!args[0] || !args[1]) usage();
132
+ await cmdCreate(args[0], args[1]);
133
+ break;
134
+
135
+ case 'create-api-trigger':
136
+ await cmdCreateApiTrigger(args[0] || 'New API Recipe');
137
+ break;
138
+
139
+ case 'update-step':
140
+ if (!args[0] || !args[1] || !args[2]) usage();
141
+ await cmdUpdateStep(args[0], args[1], args[2]);
142
+ break;
143
+
144
+ case 'put-code':
145
+ if (!args[0] || !args[1]) usage();
146
+ await cmdPutCode(args[0], args[1]);
147
+ break;
148
+
149
+ case 'start':
150
+ if (!args[0]) usage();
151
+ await cmdStart(args[0]);
152
+ break;
153
+
154
+ case 'stop':
155
+ if (!args[0]) usage();
156
+ await cmdStop(args[0]);
157
+ break;
158
+
159
+ case 'delete':
160
+ if (!args[0]) usage();
161
+ await cmdDelete(args[0]);
162
+ break;
163
+
164
+ default:
165
+ usage();
166
+ }
167
+ } catch (err) {
168
+ console.error('Error:', err.message);
169
+ process.exit(1);
170
+ }
171
+ })();
package/lib.js ADDED
@@ -0,0 +1,323 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ // ── Config ────────────────────────────────────────────────────────────────────
8
+
9
+ const _config = {
10
+ baseUrl: 'https://app.trial.workato.com/api',
11
+ token: null,
12
+ };
13
+
14
+ function loadEnv(envPath) {
15
+ const p = envPath ?? path.join(process.cwd(), '.env');
16
+ if (!fs.existsSync(p)) return;
17
+ for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
18
+ const match = line.match(/^([^#=]+)=(.*)$/);
19
+ if (match) process.env[match[1].trim()] = match[2].trim();
20
+ }
21
+ }
22
+
23
+ function setConfig(cfg) {
24
+ Object.assign(_config, cfg);
25
+ }
26
+
27
+ function getToken() {
28
+ return _config.token || process.env.WORKATO_API_TOKEN;
29
+ }
30
+
31
+ // ── HTTP helpers ──────────────────────────────────────────────────────────────
32
+
33
+ async function apiGet(urlPath) {
34
+ const res = await fetch(`${_config.baseUrl}${urlPath}`, {
35
+ headers: { 'Authorization': `Bearer ${getToken()}` },
36
+ });
37
+ if (!res.ok) throw new Error(`GET ${urlPath} failed: ${res.status} ${await res.text()}`);
38
+ return res.json();
39
+ }
40
+
41
+ async function apiPost(urlPath, body) {
42
+ const res = await fetch(`${_config.baseUrl}${urlPath}`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Authorization': `Bearer ${getToken()}`,
46
+ 'Content-Type': 'application/json',
47
+ },
48
+ body: JSON.stringify(body),
49
+ });
50
+ if (!res.ok) throw new Error(`POST ${urlPath} failed: ${res.status} ${await res.text()}`);
51
+ return res.json();
52
+ }
53
+
54
+ async function apiPut(urlPath, body) {
55
+ const res = await fetch(`${_config.baseUrl}${urlPath}`, {
56
+ method: 'PUT',
57
+ headers: {
58
+ 'Authorization': `Bearer ${getToken()}`,
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify(body),
62
+ });
63
+ if (!res.ok) throw new Error(`PUT ${urlPath} failed: ${res.status} ${await res.text()}`);
64
+ return res.json();
65
+ }
66
+
67
+ async function apiDelete(urlPath) {
68
+ const res = await fetch(`${_config.baseUrl}${urlPath}`, {
69
+ method: 'DELETE',
70
+ headers: { 'Authorization': `Bearer ${getToken()}` },
71
+ });
72
+ if (!res.ok) throw new Error(`DELETE ${urlPath} failed: ${res.status} ${await res.text()}`);
73
+ const text = await res.text();
74
+ return text ? JSON.parse(text) : {};
75
+ }
76
+
77
+ // ── Recipe code helpers ───────────────────────────────────────────────────────
78
+
79
+ // Recursively find a step by its `as` value within block arrays
80
+ function findStep(block, asId) {
81
+ for (const step of block) {
82
+ if (step.as === asId) return step;
83
+ if (step.block) {
84
+ const found = findStep(step.block, asId);
85
+ if (found) return found;
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+
91
+ // Deep merge source into target (mutates target). Arrays/primitives are replaced, objects are merged.
92
+ function deepMerge(target, source) {
93
+ for (const [key, val] of Object.entries(source)) {
94
+ if (
95
+ val && typeof val === 'object' && !Array.isArray(val) &&
96
+ target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
97
+ ) {
98
+ deepMerge(target[key], val);
99
+ } else {
100
+ target[key] = val;
101
+ }
102
+ }
103
+ }
104
+
105
+ function extractCode(data) {
106
+ const recipe = data.recipe ?? data;
107
+ return JSON.parse(recipe.code);
108
+ }
109
+
110
+ function randomHex(n) {
111
+ return crypto.randomBytes(n).toString('hex').slice(0, n);
112
+ }
113
+
114
+ function randomUUID() {
115
+ return crypto.randomUUID();
116
+ }
117
+
118
+ function apiTriggerCode() {
119
+ return {
120
+ number: 0,
121
+ provider: 'workato_api_platform',
122
+ name: 'receive_request',
123
+ as: randomHex(8),
124
+ title: null,
125
+ description: null,
126
+ keyword: 'trigger',
127
+ dynamicPickListSelection: {},
128
+ toggleCfg: {},
129
+ input: {
130
+ request: { content_type: 'json', schema: '[]' },
131
+ response: { content_type: 'json', responses: [] },
132
+ },
133
+ extended_output_schema: [],
134
+ extended_input_schema: [],
135
+ block: [],
136
+ uuid: randomUUID(),
137
+ };
138
+ }
139
+
140
+ function apiTriggerConfig() {
141
+ return [
142
+ { keyword: 'application', name: 'workato_api_platform', provider: 'workato_api_platform', skip_validation: false, account_id: null },
143
+ ];
144
+ }
145
+
146
+ // ── Read commands ─────────────────────────────────────────────────────────────
147
+
148
+ async function cmdGet(recipeId) {
149
+ const data = await apiGet(`/recipes/${recipeId}`);
150
+ const code = extractCode(data);
151
+ const outFile = `recipe_${recipeId}_code.json`;
152
+ fs.writeFileSync(outFile, JSON.stringify(code, null, 2));
153
+ console.log(JSON.stringify(code, null, 2));
154
+ console.error(`\nSaved to ${outFile}`);
155
+ return code;
156
+ }
157
+
158
+ async function cmdListRecipes(opts = {}) {
159
+ const params = new URLSearchParams();
160
+ if (opts.folder_id) params.set('folder_id', opts.folder_id);
161
+ if (opts.project_id) params.set('project_id', opts.project_id);
162
+ if (opts.page) params.set('page', opts.page);
163
+ if (opts.per_page) params.set('per_page', opts.per_page);
164
+ const qs = params.toString();
165
+ const data = await apiGet(`/recipes${qs ? '?' + qs : ''}`);
166
+ console.log(JSON.stringify(data, null, 2));
167
+ return data;
168
+ }
169
+
170
+ async function cmdListProjects() {
171
+ const data = await apiGet('/projects');
172
+ console.log(JSON.stringify(data, null, 2));
173
+ return data;
174
+ }
175
+
176
+ async function cmdListFolders(opts = {}) {
177
+ const params = new URLSearchParams();
178
+ if (opts.parent_id) params.set('parent_id', opts.parent_id);
179
+ const qs = params.toString();
180
+ const data = await apiGet(`/folders${qs ? '?' + qs : ''}`);
181
+ console.log(JSON.stringify(data, null, 2));
182
+ return data;
183
+ }
184
+
185
+ async function cmdListConnections(opts = {}) {
186
+ const params = new URLSearchParams();
187
+ if (opts.folder_id) params.set('folder_id', opts.folder_id);
188
+ const qs = params.toString();
189
+ const data = await apiGet(`/connections${qs ? '?' + qs : ''}`);
190
+ console.log(JSON.stringify(data, null, 2));
191
+ return data;
192
+ }
193
+
194
+ async function cmdListDataTables(opts = {}) {
195
+ const params = new URLSearchParams();
196
+ if (opts.project_id) params.set('project_id', opts.project_id);
197
+ const qs = params.toString();
198
+ const data = await apiGet(`/data_tables${qs ? '?' + qs : ''}`);
199
+ console.log(JSON.stringify(data, null, 2));
200
+ return data;
201
+ }
202
+
203
+ async function cmdGetDataTable(id) {
204
+ const data = await apiGet(`/data_tables/${id}`);
205
+ console.log(JSON.stringify(data, null, 2));
206
+ return data;
207
+ }
208
+
209
+ async function cmdGetJobs(recipeId, opts = {}) {
210
+ const params = new URLSearchParams();
211
+ if (opts.limit) params.set('per_page', opts.limit);
212
+ if (opts.status) params.set('status', opts.status);
213
+ const qs = params.toString();
214
+ const data = await apiGet(`/recipes/${recipeId}/jobs${qs ? '?' + qs : ''}`);
215
+ console.log(JSON.stringify(data, null, 2));
216
+ return data;
217
+ }
218
+
219
+ async function cmdGetJob(recipeId, jobId) {
220
+ const data = await apiGet(`/recipes/${recipeId}/jobs/${jobId}`);
221
+ console.log(JSON.stringify(data, null, 2));
222
+ return data;
223
+ }
224
+
225
+ // ── Write commands ────────────────────────────────────────────────────────────
226
+
227
+ async function cmdCreate(name, codeFile) {
228
+ const code = JSON.parse(fs.readFileSync(codeFile, 'utf8'));
229
+ const result = await apiPost('/recipes', {
230
+ recipe: { name, code: JSON.stringify(code) },
231
+ });
232
+ console.log(JSON.stringify(result, null, 2));
233
+ const id = (result.recipe ?? result).id;
234
+ console.error(`\nCreated recipe id: ${id}`);
235
+ return result;
236
+ }
237
+
238
+ async function cmdCreateApiTrigger(name) {
239
+ const code = apiTriggerCode();
240
+ const config = apiTriggerConfig();
241
+ const result = await apiPost('/recipes', {
242
+ recipe: { name, code: JSON.stringify(code), config: JSON.stringify(config) },
243
+ });
244
+ console.log(JSON.stringify(result, null, 2));
245
+ const id = (result.recipe ?? result).id;
246
+ console.error(`\nCreated recipe id: ${id}`);
247
+ return result;
248
+ }
249
+
250
+ async function cmdUpdateStep(recipeId, asId, patchFile) {
251
+ const patch = JSON.parse(fs.readFileSync(patchFile, 'utf8'));
252
+
253
+ const data = await apiGet(`/recipes/${recipeId}`);
254
+ const code = extractCode(data);
255
+
256
+ let step;
257
+ if (code.as === asId) {
258
+ step = code;
259
+ } else {
260
+ step = findStep(code.block ?? [], asId);
261
+ }
262
+ if (!step) throw new Error(`Step with as="${asId}" not found`);
263
+
264
+ console.error(`Found step: ${step.keyword} (as: ${asId})`);
265
+ deepMerge(step, patch);
266
+
267
+ const result = await apiPut(`/recipes/${recipeId}`, {
268
+ recipe: { code: JSON.stringify(code) },
269
+ });
270
+ console.log(JSON.stringify(result, null, 2));
271
+ console.error(`\nStep ${asId} updated successfully.`);
272
+ return result;
273
+ }
274
+
275
+ async function cmdPutCode(recipeId, codeFile) {
276
+ const code = JSON.parse(fs.readFileSync(codeFile, 'utf8'));
277
+ const result = await apiPut(`/recipes/${recipeId}`, {
278
+ recipe: { code: JSON.stringify(code) },
279
+ });
280
+ console.log(JSON.stringify(result, null, 2));
281
+ console.error('\nRecipe code replaced successfully.');
282
+ return result;
283
+ }
284
+
285
+ async function cmdStart(recipeId) {
286
+ const result = await apiPut(`/recipes/${recipeId}/start`, {});
287
+ console.log(JSON.stringify(result, null, 2));
288
+ console.error(`\nRecipe ${recipeId} started.`);
289
+ return result;
290
+ }
291
+
292
+ async function cmdStop(recipeId) {
293
+ const result = await apiPut(`/recipes/${recipeId}/stop`, {});
294
+ console.log(JSON.stringify(result, null, 2));
295
+ console.error(`\nRecipe ${recipeId} stopped.`);
296
+ return result;
297
+ }
298
+
299
+ async function cmdDelete(recipeId) {
300
+ const result = await apiDelete(`/recipes/${recipeId}`);
301
+ console.log(JSON.stringify(result, null, 2));
302
+ console.error(`\nRecipe ${recipeId} deleted.`);
303
+ return result;
304
+ }
305
+
306
+ // ── Exports ───────────────────────────────────────────────────────────────────
307
+
308
+ module.exports = {
309
+ // config
310
+ loadEnv, setConfig, getToken,
311
+ // http
312
+ apiGet, apiPost, apiPut, apiDelete,
313
+ // helpers
314
+ findStep, deepMerge, extractCode,
315
+ apiTriggerCode, apiTriggerConfig,
316
+ // read commands
317
+ cmdGet, cmdListRecipes, cmdListProjects, cmdListFolders,
318
+ cmdListConnections, cmdListDataTables, cmdGetDataTable,
319
+ cmdGetJobs, cmdGetJob,
320
+ // write commands
321
+ cmdCreate, cmdCreateApiTrigger, cmdUpdateStep, cmdPutCode,
322
+ cmdStart, cmdStop, cmdDelete,
323
+ };
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "workato-dev-api",
3
+ "version": "1.0.0",
4
+ "description": "CLI for the Workato Developer API — recipes, connections, data tables, and more",
5
+ "bin": {
6
+ "workato": "cli.js"
7
+ },
8
+ "scripts": {
9
+ "test": "node --test test/cli.test.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "keywords": [
15
+ "workato",
16
+ "cli",
17
+ "api",
18
+ "recipes",
19
+ "automation"
20
+ ],
21
+ "license": "MIT"
22
+ }