@tenonhq/dovetail-servicenow 0.0.7 → 0.0.9

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/README.md CHANGED
@@ -154,8 +154,53 @@ npx dove-sn set-related-lists \
154
154
  --table x_cadso_automate_audience \
155
155
  --related-lists "x_cadso_automate_audience_member.audience" \
156
156
  --update-set 0083c3bb33d003507b18bc534d5c7b6d
157
+
158
+ # View a flow / subflow's compiled step graph (read-only, headless)
159
+ npx dove-sn view-flow --sys-id 327c53bfc33e3250d4ddf1db05013135
160
+ npx dove-sn view-flow --sys-id <sys_id> --json --raw # structured + full model
161
+
162
+ # View a Custom Action Type's model (inputs/outputs)
163
+ npx dove-sn view-action --sys-id <action_type_sys_id> --scope <scope_sys_id>
164
+
165
+ # Copy a flow / subflow (creates an INACTIVE DRAFT) via the Designer's Copy endpoint
166
+ npx dove-sn copy-flow --sys-id <sys_id> --name "My Copy" # scope defaults to source's
167
+
168
+ # Publish (compile the snapshot of) a flow / subflow after editing in the Designer
169
+ npx dove-sn publish-flow --sys-id <sys_id> # scope defaults to the flow's
170
+
171
+ # Test a flow: validate (default, read-only) or actually run it
172
+ npx dove-sn test-flow --sys-id <sys_id> --inputs '{"phone":"+1555..."}'
173
+ npx dove-sn test-flow --sys-id <sys_id> --execute --confirm --inputs '{...}' # runs it
174
+
175
+ # Edit a flow in place (rename / description / step inputs)
176
+ echo '{"rename":{"name":"New Name"},"patchStepInputs":[{"step":"Calculate SMS Send At","input":"send_rate","value":"5"}]}' > ops.json
177
+ npx dove-sn edit-flow --sys-id <sys_id> --from-json ops.json # dry-run (diff)
178
+ npx dove-sn edit-flow --sys-id <sys_id> --from-json ops.json --apply --update-set <id> # persist
157
179
  ```
158
180
 
181
+ `copy-flow` calls the Designer's own `POST /processflow/flow/{id}/copy` — a
182
+ complete, faithful clone created as an **inactive draft**. (Don't publish +
183
+ activate a copy of a triggered production flow unless you intend it to fire.)
184
+ `test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
185
+ declared variables?) that never runs the flow; `--execute --confirm` runs it via
186
+ the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
187
+
188
+ `edit-flow` defaults to a **dry-run** diff. With `--apply`: rename/description are
189
+ written to `sys_hub_flow` through the update-set-aware API (so `--update-set` is
190
+ **required** for those), while `patchStepInputs` ride a snapshot recompile (the
191
+ `/snapshot` POST persists step input values but NOT top-level flow fields).
192
+ Step-input persistence via the snapshot POST is verified for action types but is
193
+ **best-effort for flows** — after the recompile, `edit-flow` reads the model back
194
+ and **warns** if a value didn't actually persist, so a no-op never reports as a
195
+ silent success.
196
+
197
+ `view-flow` reads `GET /api/now/processflow/flow/{id}` — the Designer's own model
198
+ endpoint — and prints the ordered, nesting-aware action + flow-logic step graph
199
+ plus the flow variables. This works for the integration user with plain basic
200
+ auth; the raw `sys_hub_flow_snapshot` Table API 404 is a row-level restriction on
201
+ the working snapshot, not a barrier. `publish-flow` POSTs the model back to
202
+ `.../flow/{id}/snapshot`, recompiling the current design (a write).
203
+
159
204
  `set-form-layout` JSON payload shape:
160
205
 
161
206
  ```json
@@ -193,10 +238,14 @@ console.log(formatLayoutResult("form layout", result));
193
238
 
194
239
  ## MCP server
195
240
 
196
- `dove-sn mcp` runs a self-contained MCP stdio server exposing the write tools to
241
+ `dove-sn mcp` runs a self-contained MCP stdio server exposing the tools to
197
242
  Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
198
- `set_related_lists`, and `add_choices_to_field`. It reads ServiceNow credentials
199
- from the same env vars as the CLI.
243
+ `set_related_lists`, `add_choices_to_field`, plus the Flow Designer tools
244
+ `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
245
+ type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
246
+ (copy a flow as an inactive draft), `flow_test` (validate or run a flow), and
247
+ `flow_edit` (patch a flow). It reads ServiceNow credentials from the same env
248
+ vars as the CLI.
200
249
 
201
250
  ```bash
202
251
  npx dove-sn mcp --smoke # list the registered tools and exit
package/dist/cli.js CHANGED
@@ -68,6 +68,13 @@ const formatter_2 = require("./layout/formatter");
68
68
  const server_1 = require("./mcp/server");
69
69
  const buildFlowOrchestrator_1 = require("./flowDesigner/buildFlowOrchestrator");
70
70
  const flowDesigner_formatter_1 = require("./flowDesigner-formatter");
71
+ const readFlow_1 = require("./flowDesigner/readFlow");
72
+ const readActionType_1 = require("./flowDesigner/readActionType");
73
+ const publishFlow_1 = require("./flowDesigner/publishFlow");
74
+ const copyFlow_1 = require("./flowDesigner/copyFlow");
75
+ const editFlow_1 = require("./flowDesigner/editFlow");
76
+ const testFlow_1 = require("./flowDesigner/testFlow");
77
+ const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
71
78
  function parseArgs(argv) {
72
79
  var command = argv[0] || "";
73
80
  var flags = {};
@@ -299,6 +306,220 @@ async function runSetRelatedLists(flags) {
299
306
  * dove-sn mcp — run the MCP stdio server. With --smoke, list the registered
300
307
  * tools and exit. Otherwise the process stays alive until the transport closes.
301
308
  */
309
+ /**
310
+ * dove-sn view-flow:
311
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
312
+ * --json Optional. Emit the structured ReadFlowResult.
313
+ * --raw Optional (with --json). Include the full processflow model.
314
+ *
315
+ * Reads the compiled flow headlessly via GET /api/now/processflow/flow/{id} and
316
+ * prints the ordered, nesting-aware step graph + flow variables. Read-only.
317
+ */
318
+ async function runViewFlow(flags) {
319
+ var sysId = flags["sys-id"] || flags.sysId;
320
+ if (!sysId) {
321
+ process.stderr.write("view-flow: --sys-id <sys_id> is required\n");
322
+ return 1;
323
+ }
324
+ var client = (0, client_1.createClient)({});
325
+ var result = await (0, readFlow_1.readFlow)({ client: client, sysId: sysId, raw: flags.raw === "true" });
326
+ if (flags.json === "true") {
327
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
328
+ return 0;
329
+ }
330
+ process.stdout.write((0, flowDesigner_formatter_2.formatReadFlowResult)(result) + "\n");
331
+ return 0;
332
+ }
333
+ /**
334
+ * dove-sn view-action:
335
+ * --sys-id <sys_id> Required. sys_hub_action_type_definition sys_id.
336
+ * --scope <sys_id> Required. Application scope (sysparm_transaction_scope).
337
+ * --json [--raw] Optional. Structured ReadActionTypeResult / full model.
338
+ *
339
+ * Reads a Custom Action Type's compiled model (identity, inputs, outputs). Read-only.
340
+ */
341
+ async function runViewAction(flags) {
342
+ var sysId = flags["sys-id"] || flags.sysId;
343
+ var scope = flags.scope || flags.scopeSysId;
344
+ if (!sysId || !scope) {
345
+ process.stderr.write("view-action: --sys-id <sys_id> and --scope <sys_id> are required\n");
346
+ return 1;
347
+ }
348
+ var client = (0, client_1.createClient)({});
349
+ var result = await (0, readActionType_1.readActionType)({
350
+ client: client,
351
+ sysId: sysId,
352
+ scopeSysId: scope,
353
+ raw: flags.raw === "true"
354
+ });
355
+ if (flags.json === "true") {
356
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
357
+ return 0;
358
+ }
359
+ process.stdout.write((0, flowDesigner_formatter_2.formatReadActionTypeResult)(result) + "\n");
360
+ return 0;
361
+ }
362
+ /**
363
+ * dove-sn publish-flow:
364
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow) to publish.
365
+ * --scope <sys_id> Optional. sysparm_transaction_scope (defaults to the model's scope).
366
+ * --json Optional. Emit the structured PublishFlowResult.
367
+ *
368
+ * Compiles the flow's snapshot via POST /api/now/processflow/flow/{id}/snapshot —
369
+ * a WRITE that recompiles the current design. Use the Designer to edit, then this
370
+ * to publish. (For edited content, the library publishFlow accepts a model.)
371
+ */
372
+ async function runPublishFlow(flags) {
373
+ var sysId = flags["sys-id"] || flags.sysId;
374
+ if (!sysId) {
375
+ process.stderr.write("publish-flow: --sys-id <sys_id> is required\n");
376
+ return 1;
377
+ }
378
+ var params = {
379
+ client: (0, client_1.createClient)({}),
380
+ sysId: sysId
381
+ };
382
+ if (flags.scope || flags.scopeSysId) {
383
+ params.scopeSysId = flags.scope || flags.scopeSysId;
384
+ }
385
+ var result = await (0, publishFlow_1.publishFlow)(params);
386
+ if (flags.json === "true") {
387
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
388
+ return 0;
389
+ }
390
+ process.stdout.write("Published flow " + sysId + " (HTTP " + result.httpStatus + ")"
391
+ + (result.snapshotSysId ? " — snapshot " + result.snapshotSysId : "") + "\n");
392
+ return 0;
393
+ }
394
+ /**
395
+ * dove-sn copy-flow:
396
+ * --sys-id <sys_id> Required. Source sys_hub_flow sys_id (flow or subflow).
397
+ * --name <name> Required. Name for the copy.
398
+ * --scope <sys_id> Optional. Target scope (defaults to the source's scope).
399
+ * --json Optional. Emit the structured CopyFlowResult.
400
+ *
401
+ * Copies the flow via the Designer's own Copy endpoint — a complete, faithful
402
+ * clone created as an INACTIVE DRAFT. Publish it with publish-flow when ready.
403
+ * (Do NOT publish + activate a copy of a triggered production flow unless you
404
+ * intend it to fire.)
405
+ */
406
+ async function runCopyFlow(flags) {
407
+ var sysId = flags["sys-id"] || flags.sysId;
408
+ var name = flags.name;
409
+ if (!sysId || !name) {
410
+ process.stderr.write("copy-flow: --sys-id <sys_id> and --name <name> are required\n");
411
+ return 1;
412
+ }
413
+ var params = { client: (0, client_1.createClient)({}), sourceSysId: sysId, newName: name };
414
+ if (flags.scope || flags.scopeSysId) {
415
+ params.scopeSysId = flags.scope || flags.scopeSysId;
416
+ }
417
+ var result = await (0, copyFlow_1.copyFlow)(params);
418
+ if (flags.json === "true") {
419
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
420
+ return 0;
421
+ }
422
+ process.stdout.write("Copied to '" + result.name + "' (sys_id " + result.sysId + ", scope " + result.scopeSysId
423
+ + ") — inactive draft. Publish with: dove-sn publish-flow --sys-id " + result.sysId + "\n");
424
+ return 0;
425
+ }
426
+ /**
427
+ * dove-sn test-flow:
428
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
429
+ * --execute Optional. Actually run it (default is validate-only).
430
+ * --confirm Required with --execute. A deliberate run-for-real gate.
431
+ * --inputs <json> Optional. JSON object of inputs (or --inputs-json <path>).
432
+ * --json Optional. Emit the structured TestFlowResult.
433
+ *
434
+ * Default (no --execute) is a safe pre-flight: published? readable? inputs match
435
+ * declared variables? --execute POSTs the FlowAPI runner endpoint (see
436
+ * resources/runFlow.md). Executing a flow can cause real side effects.
437
+ */
438
+ async function runTestFlow(flags) {
439
+ var sysId = flags["sys-id"] || flags.sysId;
440
+ if (!sysId) {
441
+ process.stderr.write("test-flow: --sys-id <sys_id> is required\n");
442
+ return 1;
443
+ }
444
+ var inputs = {};
445
+ if (flags["inputs-json"]) {
446
+ inputs = JSON.parse(fs.readFileSync(flags["inputs-json"], "utf8"));
447
+ }
448
+ else if (flags.inputs) {
449
+ inputs = JSON.parse(flags.inputs);
450
+ }
451
+ var params = {
452
+ client: (0, client_1.createClient)({}),
453
+ sysId: sysId,
454
+ mode: flags.execute === "true" ? "execute" : "validate",
455
+ inputs: inputs,
456
+ confirm: flags.confirm === "true"
457
+ };
458
+ if (flags.runner) {
459
+ params.runnerPath = flags.runner;
460
+ }
461
+ var result = await (0, testFlow_1.testFlow)(params);
462
+ if (flags.json === "true") {
463
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
464
+ return 0;
465
+ }
466
+ process.stdout.write("[" + result.mode + "] ok=" + result.ok + "\n");
467
+ for (var i = 0; i < result.notes.length; i += 1) {
468
+ process.stdout.write(" " + result.notes[i] + "\n");
469
+ }
470
+ return result.ok ? 0 : 2;
471
+ }
472
+ /**
473
+ * dove-sn edit-flow:
474
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
475
+ * --from-json <path> Required. JSON EditFlowOps { rename?, description?, patchStepInputs? }.
476
+ * --apply Optional. Persist the edit (default is a dry-run diff).
477
+ * --scope <sys_id> Optional. sysparm_transaction_scope for the publish.
478
+ * --update-set <sys_id> Required with --apply when ops include rename/description.
479
+ * --json Optional. Emit the structured EditFlowResult.
480
+ *
481
+ * Reads the model, applies the declarative edits, and (with --apply) persists them:
482
+ * rename/description via the update-set-aware record write, step inputs via a
483
+ * snapshot recompile. Without --apply it prints the would-be changes.
484
+ */
485
+ async function runEditFlow(flags) {
486
+ var sysId = flags["sys-id"] || flags.sysId;
487
+ if (!sysId) {
488
+ process.stderr.write("edit-flow: --sys-id <sys_id> is required\n");
489
+ return 1;
490
+ }
491
+ if (!flags["from-json"]) {
492
+ process.stderr.write("edit-flow: --from-json <path> (EditFlowOps) is required\n");
493
+ return 1;
494
+ }
495
+ var ops = JSON.parse(fs.readFileSync(flags["from-json"], "utf8"));
496
+ var params = {
497
+ client: (0, client_1.createClient)({}),
498
+ sysId: sysId,
499
+ ops: ops,
500
+ apply: flags.apply === "true"
501
+ };
502
+ if (flags.scope || flags.scopeSysId) {
503
+ params.scopeSysId = flags.scope || flags.scopeSysId;
504
+ }
505
+ if (flags["update-set"] || flags.updateSetSysId) {
506
+ params.updateSetSysId = flags["update-set"] || flags.updateSetSysId;
507
+ }
508
+ var result = await (0, editFlow_1.editFlow)(params);
509
+ if (flags.json === "true") {
510
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
511
+ return 0;
512
+ }
513
+ process.stdout.write("[" + result.status + "] " + result.changes.length + " change(s)"
514
+ + (result.snapshotSysId ? " — snapshot " + result.snapshotSysId : "") + "\n");
515
+ for (var i = 0; i < result.changes.length; i += 1) {
516
+ process.stdout.write(" + " + result.changes[i] + "\n");
517
+ }
518
+ for (var w = 0; w < result.warnings.length; w += 1) {
519
+ process.stdout.write(" ! " + result.warnings[w] + "\n");
520
+ }
521
+ return 0;
522
+ }
302
523
  async function runMcp(flags) {
303
524
  if (flags.smoke === "true") {
304
525
  await (0, server_1.runSmoke)();
@@ -324,6 +545,18 @@ function printHelp() {
324
545
  " [--view <v>] [--scope <s>] [--prune false] [--dry-run] [--json])\n" +
325
546
  " build-flow Author Custom Action Types and Subflows from a JSON spec\n" +
326
547
  " (--from-json <path> [--update-set <sys_id>] [--dry-run] [--skip-publish] [--json])\n" +
548
+ " view-flow Read a flow/subflow's compiled step graph (read-only)\n" +
549
+ " (--sys-id <sys_id> [--json] [--raw])\n" +
550
+ " view-action Read a Custom Action Type's model — inputs/outputs (read-only)\n" +
551
+ " (--sys-id <sys_id> --scope <sys_id> [--json] [--raw])\n" +
552
+ " publish-flow Compile a flow/subflow snapshot (write)\n" +
553
+ " (--sys-id <sys_id> [--scope <sys_id>] [--json])\n" +
554
+ " copy-flow Copy a flow/subflow (inactive draft) via the Designer Copy API\n" +
555
+ " (--sys-id <sys_id> --name <name> [--scope <sys_id>] [--json])\n" +
556
+ " test-flow Validate (default) or run a flow/subflow\n" +
557
+ " (--sys-id <sys_id> [--execute --confirm] [--inputs <json>] [--json])\n" +
558
+ " edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
559
+ " (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
327
560
  " mcp Run the MCP stdio server (--smoke lists tools and exits)\n");
328
561
  }
329
562
  async function main() {
@@ -335,6 +568,24 @@ async function main() {
335
568
  if (parsed.command === "build-flow") {
336
569
  return await runBuildFlowCmd(parsed.flags);
337
570
  }
571
+ if (parsed.command === "view-flow") {
572
+ return await runViewFlow(parsed.flags);
573
+ }
574
+ if (parsed.command === "view-action") {
575
+ return await runViewAction(parsed.flags);
576
+ }
577
+ if (parsed.command === "publish-flow") {
578
+ return await runPublishFlow(parsed.flags);
579
+ }
580
+ if (parsed.command === "copy-flow") {
581
+ return await runCopyFlow(parsed.flags);
582
+ }
583
+ if (parsed.command === "test-flow") {
584
+ return await runTestFlow(parsed.flags);
585
+ }
586
+ if (parsed.command === "edit-flow") {
587
+ return await runEditFlow(parsed.flags);
588
+ }
338
589
  if (parsed.command === "create-view") {
339
590
  await runCreateView(parsed.flags);
340
591
  return 0;
@@ -29,6 +29,15 @@ export interface BuildFlowOptions {
29
29
  skipPublish?: boolean;
30
30
  /** Forwarded to triggerPublication. Useful for tight tests; defaults to its own 15s. */
31
31
  snapshotTimeoutMs?: number;
32
+ /**
33
+ * Steps fixture for publishing an action-type clone. The action-type model GET
34
+ * returns `steps: null`, so the real publisher (publishActionType) needs a
35
+ * steps array. When provided for kind="actionType", the orchestrator uses the
36
+ * real processflow publisher; without it, action-type publish falls back to
37
+ * the degraded triggerPublication. Ignored for subflows (publishFlow re-posts
38
+ * the full model, which already carries the instance graph).
39
+ */
40
+ steps?: Array<Record<string, any>>;
32
41
  }
33
42
  /**
34
43
  * Outcome categories. Maps directly to CLI exit codes:
@@ -14,6 +14,8 @@ const cloneSubflow_1 = require("./cloneSubflow");
14
14
  const cloneActionType_1 = require("./cloneActionType");
15
15
  const verifyArtifact_1 = require("./verifyArtifact");
16
16
  const triggerPublication_1 = require("./triggerPublication");
17
+ const publishFlow_1 = require("./publishFlow");
18
+ const publishActionType_1 = require("./publishActionType");
17
19
  const RX_SYS_ID = /^[0-9a-f]{32}$/;
18
20
  function unrecoverable(spec, stage, message) {
19
21
  return {
@@ -23,6 +25,50 @@ function unrecoverable(spec, stage, message) {
23
25
  error: { stage: stage, message: message },
24
26
  };
25
27
  }
28
+ /**
29
+ * Publish a freshly cloned artifact. Tries the real processflow publisher first
30
+ * (publishFlow for subflows, publishActionType for action types when a steps
31
+ * fixture is supplied), and falls back to the degraded triggerPublication if the
32
+ * real path is unavailable or throws. Never throws — always returns a
33
+ * TriggerPublicationResult-compatible object so the caller's outcome mapping is
34
+ * unchanged (status "published" => done; anything else => needs-ui-publish).
35
+ */
36
+ async function publishArtifact(client, spec, sysId, opts) {
37
+ try {
38
+ if (spec.kind === "subflow") {
39
+ var pf = await (0, publishFlow_1.publishFlow)({ client: client, sysId: sysId, scopeSysId: spec.newScope });
40
+ return { status: "published", snapshotSysId: pf.snapshotSysId, pushSucceeded: true };
41
+ }
42
+ // actionType: the real publisher needs a steps fixture (model GET => steps:null).
43
+ if (spec.kind === "actionType" && opts.steps && opts.steps.length > 0) {
44
+ var pa = await (0, publishActionType_1.publishActionType)({
45
+ client: client,
46
+ sysId: sysId,
47
+ scopeSysId: spec.newScope,
48
+ steps: opts.steps,
49
+ });
50
+ return { status: "published", snapshotSysId: pa.snapshotSysId, pushSucceeded: true };
51
+ }
52
+ }
53
+ catch (err) {
54
+ // Real publish failed — fall through to the degraded path below, which is
55
+ // engineered to surface a UI publish URL rather than throw.
56
+ }
57
+ try {
58
+ return await (0, triggerPublication_1.triggerPublication)({
59
+ client: client,
60
+ sysId: sysId,
61
+ kind: spec.kind,
62
+ updateSetSysId: spec.updateSetSysId,
63
+ snapshotTimeoutMs: opts.snapshotTimeoutMs,
64
+ });
65
+ }
66
+ catch (err) {
67
+ // triggerPublication is engineered to never throw on the happy path, but
68
+ // defensive: treat unexpected throws as needs-ui-publish.
69
+ return { status: "needs-ui-publish", pushSucceeded: false, uiPublishUrl: undefined };
70
+ }
71
+ }
26
72
  function validateSpec(spec) {
27
73
  if (!spec || typeof spec !== "object")
28
74
  throw new Error("spec must be a JSON object");
@@ -163,27 +209,14 @@ async function runBuildFlow(client, rawSpec, opts = {}) {
163
209
  verify: verify,
164
210
  };
165
211
  }
166
- // Phase 4: publication (best-effort, degraded mode until Phase 0 lands).
212
+ // Phase 4: publication. Try the real processflow publisher first (compiles
213
+ // the snapshot via the Designer's own Publish endpoint — basic auth, 2xx),
214
+ // and fall back to the degraded triggerPublication (status flip + poll, UI
215
+ // fallback) only if the real path throws. This is what replaces the long-
216
+ // standing "needs-ui-publish" default for the common case.
167
217
  var publish;
168
218
  if (!opts.skipPublish) {
169
- try {
170
- publish = await (0, triggerPublication_1.triggerPublication)({
171
- client: client,
172
- sysId: cloneResult.sysId,
173
- kind: spec.kind,
174
- updateSetSysId: spec.updateSetSysId,
175
- snapshotTimeoutMs: opts.snapshotTimeoutMs,
176
- });
177
- }
178
- catch (err) {
179
- // triggerPublication is engineered to never throw on the happy path,
180
- // but defensive: treat unexpected throws as needs-ui-publish.
181
- publish = {
182
- status: "needs-ui-publish",
183
- pushSucceeded: false,
184
- uiPublishUrl: undefined,
185
- };
186
- }
219
+ publish = await publishArtifact(client, spec, cloneResult.sysId, opts);
187
220
  }
188
221
  var artifact = {
189
222
  sysId: cloneResult.sysId,
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Copy a Flow Designer flow or subflow — the Designer's own "Copy flow" action,
3
+ * headless.
4
+ *
5
+ * The Workflow Studio "Copy flow" button calls a clean REST endpoint (captured
6
+ * from a HAR, same family as the publish/snapshot endpoint), which works for the
7
+ * integration user with basic auth:
8
+ *
9
+ * POST /api/now/processflow/flow/{sourceSysId}/copy?sysparm_transaction_scope={scope}
10
+ * body: { name: "<new flow name>", scope: "<target scope sys_id>" }
11
+ * -> 200, { result: { data: "<new flow sys_id>" } } — the copy is a complete,
12
+ * faithful clone (trigger + all actions + variables + properties), created
13
+ * as an INACTIVE DRAFT.
14
+ *
15
+ * This supersedes the record-graph clone (cloneSubflow), which can't work for
16
+ * the integration user: the design tables (sys_hub_flow_input/action_instance)
17
+ * ignore sysparm_query on the plain Table API, and the build-agent endpoint that
18
+ * honors the filter returns 401 without the Build Agent role — so a record-graph
19
+ * read either 401s or scans the whole table. The platform copy endpoint sidesteps
20
+ * all of that: ServiceNow assembles the copy server-side.
21
+ *
22
+ * The copy lands in `scopeSysId` (sysparm_transaction_scope) — which may differ
23
+ * from the source's scope — as an inactive draft. Publish it with publishFlow
24
+ * when ready. Do NOT publish + activate a copy of a triggered production flow
25
+ * unless you intend it to fire (e.g. a Send-SMS flow would duplicate sends).
26
+ */
27
+ import type { ServiceNowClient } from "../client";
28
+ export interface CopyFlowParams {
29
+ client: ServiceNowClient;
30
+ /** sys_id of the sys_hub_flow (flow or subflow) to copy. */
31
+ sourceSysId: string;
32
+ /** Name for the new copy. */
33
+ newName: string;
34
+ /**
35
+ * Target application scope sys_id (sysparm_transaction_scope) the copy is
36
+ * created in. Optional — defaults to the source flow's own scope (read first).
37
+ */
38
+ scopeSysId?: string;
39
+ }
40
+ export interface CopyFlowResult {
41
+ /** sys_id of the newly created copy. */
42
+ sysId: string;
43
+ /** Name of the copy (echoes newName). */
44
+ name: string;
45
+ /** The scope the copy was created in. */
46
+ scopeSysId: string;
47
+ }
48
+ export declare function copyFlow(params: CopyFlowParams): Promise<CopyFlowResult>;
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * Copy a Flow Designer flow or subflow — the Designer's own "Copy flow" action,
4
+ * headless.
5
+ *
6
+ * The Workflow Studio "Copy flow" button calls a clean REST endpoint (captured
7
+ * from a HAR, same family as the publish/snapshot endpoint), which works for the
8
+ * integration user with basic auth:
9
+ *
10
+ * POST /api/now/processflow/flow/{sourceSysId}/copy?sysparm_transaction_scope={scope}
11
+ * body: { name: "<new flow name>", scope: "<target scope sys_id>" }
12
+ * -> 200, { result: { data: "<new flow sys_id>" } } — the copy is a complete,
13
+ * faithful clone (trigger + all actions + variables + properties), created
14
+ * as an INACTIVE DRAFT.
15
+ *
16
+ * This supersedes the record-graph clone (cloneSubflow), which can't work for
17
+ * the integration user: the design tables (sys_hub_flow_input/action_instance)
18
+ * ignore sysparm_query on the plain Table API, and the build-agent endpoint that
19
+ * honors the filter returns 401 without the Build Agent role — so a record-graph
20
+ * read either 401s or scans the whole table. The platform copy endpoint sidesteps
21
+ * all of that: ServiceNow assembles the copy server-side.
22
+ *
23
+ * The copy lands in `scopeSysId` (sysparm_transaction_scope) — which may differ
24
+ * from the source's scope — as an inactive draft. Publish it with publishFlow
25
+ * when ready. Do NOT publish + activate a copy of a triggered production flow
26
+ * unless you intend it to fire (e.g. a Send-SMS flow would duplicate sends).
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.copyFlow = copyFlow;
30
+ /**
31
+ * Pull the new flow's sys_id from the copy response. The endpoint returns the
32
+ * new sys_id as a bare string under `result.data`; older/alternate shapes may
33
+ * nest it under a model object's `id`/`sys_id`.
34
+ */
35
+ function extractCopiedSysId(resp) {
36
+ if (resp && typeof resp === "object" && resp.result && typeof resp.result === "object") {
37
+ var d = resp.result.data;
38
+ if (typeof d === "string" && d.length >= 32) {
39
+ return d;
40
+ }
41
+ if (d && typeof d === "object") {
42
+ if (typeof d.id === "string")
43
+ return d.id;
44
+ if (typeof d.sys_id === "string")
45
+ return d.sys_id;
46
+ }
47
+ }
48
+ if (resp && typeof resp === "object") {
49
+ if (typeof resp.id === "string")
50
+ return resp.id;
51
+ if (typeof resp.sys_id === "string")
52
+ return resp.sys_id;
53
+ }
54
+ return "";
55
+ }
56
+ function flowGetPath(sysId) {
57
+ return "/api/now/processflow/flow/" + encodeURIComponent(sysId);
58
+ }
59
+ function copyPath(sourceSysId, scopeSysId) {
60
+ return "/api/now/processflow/flow/" + encodeURIComponent(sourceSysId) + "/copy"
61
+ + "?sysparm_transaction_scope=" + encodeURIComponent(scopeSysId);
62
+ }
63
+ function unwrapModel(data) {
64
+ if (data && typeof data === "object" && data.result && typeof data.result === "object") {
65
+ if (data.result.data && typeof data.result.data === "object") {
66
+ return data.result.data;
67
+ }
68
+ return data.result;
69
+ }
70
+ if (data && typeof data === "object" && data.data && typeof data.data === "object") {
71
+ return data.data;
72
+ }
73
+ return data;
74
+ }
75
+ async function copyFlow(params) {
76
+ var client = params.client;
77
+ var sourceSysId = params.sourceSysId;
78
+ if (!sourceSysId) {
79
+ throw new Error("copyFlow: sourceSysId is required.");
80
+ }
81
+ if (typeof params.newName !== "string" || params.newName.trim().length === 0) {
82
+ throw new Error("copyFlow: newName is required.");
83
+ }
84
+ // Resolve the target scope: explicit param, else the source flow's own scope.
85
+ var scopeSysId = params.scopeSysId;
86
+ if (!scopeSysId) {
87
+ var src = unwrapModel(await client.now.get(flowGetPath(sourceSysId)));
88
+ if (src && typeof src.scope === "string") {
89
+ scopeSysId = src.scope;
90
+ }
91
+ }
92
+ if (!scopeSysId) {
93
+ throw new Error("copyFlow: scopeSysId is required and the source flow carried no `scope` to default from.");
94
+ }
95
+ var resp = await client.now.post(copyPath(sourceSysId, scopeSysId), { name: params.newName, scope: scopeSysId });
96
+ var newSysId = extractCopiedSysId(resp);
97
+ if (!newSysId) {
98
+ throw new Error("copyFlow: the copy succeeded but no new flow sys_id was found in the response: "
99
+ + JSON.stringify(resp).substring(0, 300));
100
+ }
101
+ return {
102
+ sysId: newSysId,
103
+ name: params.newName,
104
+ scopeSysId: scopeSysId
105
+ };
106
+ }