@tenonhq/dovetail-servicenow 0.0.6 → 0.0.8

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,40 @@ 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
+ # Publish (compile the snapshot of) a flow / subflow after editing in the Designer
166
+ npx dove-sn publish-flow --sys-id <sys_id> # scope defaults to the flow's
167
+
168
+ # Test a flow: validate (default, read-only) or actually run it
169
+ npx dove-sn test-flow --sys-id <sys_id> --inputs '{"phone":"+1555..."}'
170
+ npx dove-sn test-flow --sys-id <sys_id> --execute --confirm --inputs '{...}' # runs it
171
+
172
+ # Edit a flow in place (rename / description / step inputs), then publish
173
+ echo '{"rename":{"name":"New Name"},"patchStepInputs":[{"step":"Calculate SMS Send At","input":"send_rate","value":"5"}]}' > ops.json
174
+ npx dove-sn edit-flow --sys-id <sys_id> --from-json ops.json # dry-run (diff)
175
+ npx dove-sn edit-flow --sys-id <sys_id> --from-json ops.json --apply # publish the edit
157
176
  ```
158
177
 
178
+ `test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
179
+ declared variables?) that never runs the flow. `--execute --confirm` runs it via
180
+ the server-side FlowAPI runner (deploy `resources/runFlow.md` first); running a
181
+ flow can cause real side effects. `edit-flow` defaults to a **dry-run** diff;
182
+ `--apply` re-publishes the patched model via the snapshot endpoint.
183
+
184
+ `view-flow` reads `GET /api/now/processflow/flow/{id}` — the Designer's own model
185
+ endpoint — and prints the ordered, nesting-aware action + flow-logic step graph
186
+ plus the flow variables. This works for the integration user with plain basic
187
+ auth; the raw `sys_hub_flow_snapshot` Table API 404 is a row-level restriction on
188
+ the working snapshot, not a barrier. `publish-flow` POSTs the model back to
189
+ `.../flow/{id}/snapshot`, recompiling the current design (a write).
190
+
159
191
  `set-form-layout` JSON payload shape:
160
192
 
161
193
  ```json
@@ -193,10 +225,13 @@ console.log(formatLayoutResult("form layout", result));
193
225
 
194
226
  ## MCP server
195
227
 
196
- `dove-sn mcp` runs a self-contained MCP stdio server exposing the write tools to
228
+ `dove-sn mcp` runs a self-contained MCP stdio server exposing the tools to
197
229
  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.
230
+ `set_related_lists`, `add_choices_to_field`, plus the Flow Designer tools
231
+ `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
232
+ type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_test`
233
+ (validate or run a flow), and `flow_edit` (patch a flow + publish). It reads
234
+ ServiceNow credentials from the same env vars as the CLI.
200
235
 
201
236
  ```bash
202
237
  npx dove-sn mcp --smoke # list the registered tools and exit
@@ -212,6 +247,53 @@ npx dove-sn mcp # run the stdio server (wire into .mcp.json)
212
247
  This server is separate from `@tenonhq/dovetail-mcp` (the read-only cross-system
213
248
  aggregator) — `dovetail-servicenow`'s server is the ServiceNow **write** surface.
214
249
 
250
+ ## Publishing a Custom Action Type
251
+
252
+ `publishActionType` compiles the `sys_hub_flow_snapshot` for a Custom Action Type
253
+ — the step that makes it draggable in the Flow Designer palette. This replays the
254
+ Designer's **Publish** button, which is a plain REST call that **works with basic
255
+ auth** (no session cookie, CSRF token, or `sn_build_agent` role):
256
+
257
+ ```
258
+ GET /api/now/processflow/action/action_types/{sysId}?sysparm_transaction_scope={scope}
259
+ -> 200, the full action-type model EXCEPT `steps` (always returns null)
260
+ POST /api/now/processflow/action/action_types/{sysId}/snapshot?sysparm_transaction_scope={scope}
261
+ body = the model with a `steps` array grafted in
262
+ -> 201 Created (compiles the snapshot; also persists step input values
263
+ back to sys_variable_value)
264
+ ```
265
+
266
+ This is the **real** snapshot compiler and supersedes `triggerPublication` for
267
+ action types — that function ships in degraded mode (it only sets
268
+ `status="published"` and polls, because the snapshot trigger was unknown when it
269
+ was written). `triggerPublication` is retained for back-compat and the subflow path.
270
+
271
+ ```ts
272
+ import { createClient, publishActionType } from "@tenonhq/dovetail-servicenow";
273
+
274
+ var client = createClient({});
275
+ var result = await publishActionType({
276
+ client: client,
277
+ sysId: "60e6743e33814bd07b18bc534d5c7b9e", // sys_hub_action_type_definition
278
+ scopeSysId: "cd61acbbc3c85a1085b196c4e40131bd", // sysparm_transaction_scope
279
+ steps: require("./fixtures/my-action.steps.json") // see caveat below
280
+ });
281
+ // { status: "published", httpStatus: 201, snapshotSysId?: "..." }
282
+ ```
283
+
284
+ ### Steps-fixture caveat (required)
285
+
286
+ The GET returns **`steps: null`** even for an already-published action — the
287
+ Designer assembles `steps` client-side from the step records. So to publish you
288
+ **must supply a `steps` fixture** via `params.steps`. Each step's `action` field
289
+ is remapped to the target `sysId` automatically. If you omit `steps` and the
290
+ fetched model has no usable `steps` array, `publishActionType` throws with a clear
291
+ message. For a faithful clone, capture the source action's `steps` from a HAR of
292
+ the Publish call and store it as a fixture beside your driver.
293
+
294
+ Full recipe and the 6-record action-type graph:
295
+ `docs/servicenow-flow-designer-headless-authoring.md` in the CTO repo.
296
+
215
297
  ## Roadmap
216
298
 
217
299
  The same query-to-diff pattern will continue across the rest of the
package/dist/cli.js CHANGED
@@ -68,6 +68,12 @@ 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 editFlow_1 = require("./flowDesigner/editFlow");
75
+ const testFlow_1 = require("./flowDesigner/testFlow");
76
+ const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
71
77
  function parseArgs(argv) {
72
78
  var command = argv[0] || "";
73
79
  var flags = {};
@@ -299,6 +305,183 @@ async function runSetRelatedLists(flags) {
299
305
  * dove-sn mcp — run the MCP stdio server. With --smoke, list the registered
300
306
  * tools and exit. Otherwise the process stays alive until the transport closes.
301
307
  */
308
+ /**
309
+ * dove-sn view-flow:
310
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
311
+ * --json Optional. Emit the structured ReadFlowResult.
312
+ * --raw Optional (with --json). Include the full processflow model.
313
+ *
314
+ * Reads the compiled flow headlessly via GET /api/now/processflow/flow/{id} and
315
+ * prints the ordered, nesting-aware step graph + flow variables. Read-only.
316
+ */
317
+ async function runViewFlow(flags) {
318
+ var sysId = flags["sys-id"] || flags.sysId;
319
+ if (!sysId) {
320
+ process.stderr.write("view-flow: --sys-id <sys_id> is required\n");
321
+ return 1;
322
+ }
323
+ var client = (0, client_1.createClient)({});
324
+ var result = await (0, readFlow_1.readFlow)({ client: client, sysId: sysId, raw: flags.raw === "true" });
325
+ if (flags.json === "true") {
326
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
327
+ return 0;
328
+ }
329
+ process.stdout.write((0, flowDesigner_formatter_2.formatReadFlowResult)(result) + "\n");
330
+ return 0;
331
+ }
332
+ /**
333
+ * dove-sn view-action:
334
+ * --sys-id <sys_id> Required. sys_hub_action_type_definition sys_id.
335
+ * --scope <sys_id> Required. Application scope (sysparm_transaction_scope).
336
+ * --json [--raw] Optional. Structured ReadActionTypeResult / full model.
337
+ *
338
+ * Reads a Custom Action Type's compiled model (identity, inputs, outputs). Read-only.
339
+ */
340
+ async function runViewAction(flags) {
341
+ var sysId = flags["sys-id"] || flags.sysId;
342
+ var scope = flags.scope || flags.scopeSysId;
343
+ if (!sysId || !scope) {
344
+ process.stderr.write("view-action: --sys-id <sys_id> and --scope <sys_id> are required\n");
345
+ return 1;
346
+ }
347
+ var client = (0, client_1.createClient)({});
348
+ var result = await (0, readActionType_1.readActionType)({
349
+ client: client,
350
+ sysId: sysId,
351
+ scopeSysId: scope,
352
+ raw: flags.raw === "true"
353
+ });
354
+ if (flags.json === "true") {
355
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
356
+ return 0;
357
+ }
358
+ process.stdout.write((0, flowDesigner_formatter_2.formatReadActionTypeResult)(result) + "\n");
359
+ return 0;
360
+ }
361
+ /**
362
+ * dove-sn publish-flow:
363
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow) to publish.
364
+ * --scope <sys_id> Optional. sysparm_transaction_scope (defaults to the model's scope).
365
+ * --json Optional. Emit the structured PublishFlowResult.
366
+ *
367
+ * Compiles the flow's snapshot via POST /api/now/processflow/flow/{id}/snapshot —
368
+ * a WRITE that recompiles the current design. Use the Designer to edit, then this
369
+ * to publish. (For edited content, the library publishFlow accepts a model.)
370
+ */
371
+ async function runPublishFlow(flags) {
372
+ var sysId = flags["sys-id"] || flags.sysId;
373
+ if (!sysId) {
374
+ process.stderr.write("publish-flow: --sys-id <sys_id> is required\n");
375
+ return 1;
376
+ }
377
+ var params = {
378
+ client: (0, client_1.createClient)({}),
379
+ sysId: sysId
380
+ };
381
+ if (flags.scope || flags.scopeSysId) {
382
+ params.scopeSysId = flags.scope || flags.scopeSysId;
383
+ }
384
+ var result = await (0, publishFlow_1.publishFlow)(params);
385
+ if (flags.json === "true") {
386
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
387
+ return 0;
388
+ }
389
+ process.stdout.write("Published flow " + sysId + " (HTTP " + result.httpStatus + ")"
390
+ + (result.snapshotSysId ? " — snapshot " + result.snapshotSysId : "") + "\n");
391
+ return 0;
392
+ }
393
+ /**
394
+ * dove-sn test-flow:
395
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
396
+ * --execute Optional. Actually run it (default is validate-only).
397
+ * --confirm Required with --execute. A deliberate run-for-real gate.
398
+ * --inputs <json> Optional. JSON object of inputs (or --inputs-json <path>).
399
+ * --json Optional. Emit the structured TestFlowResult.
400
+ *
401
+ * Default (no --execute) is a safe pre-flight: published? readable? inputs match
402
+ * declared variables? --execute POSTs the FlowAPI runner endpoint (see
403
+ * resources/runFlow.md). Executing a flow can cause real side effects.
404
+ */
405
+ async function runTestFlow(flags) {
406
+ var sysId = flags["sys-id"] || flags.sysId;
407
+ if (!sysId) {
408
+ process.stderr.write("test-flow: --sys-id <sys_id> is required\n");
409
+ return 1;
410
+ }
411
+ var inputs = {};
412
+ if (flags["inputs-json"]) {
413
+ inputs = JSON.parse(fs.readFileSync(flags["inputs-json"], "utf8"));
414
+ }
415
+ else if (flags.inputs) {
416
+ inputs = JSON.parse(flags.inputs);
417
+ }
418
+ var params = {
419
+ client: (0, client_1.createClient)({}),
420
+ sysId: sysId,
421
+ mode: flags.execute === "true" ? "execute" : "validate",
422
+ inputs: inputs,
423
+ confirm: flags.confirm === "true"
424
+ };
425
+ if (flags.runner) {
426
+ params.runnerPath = flags.runner;
427
+ }
428
+ var result = await (0, testFlow_1.testFlow)(params);
429
+ if (flags.json === "true") {
430
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
431
+ return 0;
432
+ }
433
+ process.stdout.write("[" + result.mode + "] ok=" + result.ok + "\n");
434
+ for (var i = 0; i < result.notes.length; i += 1) {
435
+ process.stdout.write(" " + result.notes[i] + "\n");
436
+ }
437
+ return result.ok ? 0 : 2;
438
+ }
439
+ /**
440
+ * dove-sn edit-flow:
441
+ * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
442
+ * --from-json <path> Required. JSON EditFlowOps { rename?, description?, patchStepInputs? }.
443
+ * --apply Optional. Publish the edit (default is a dry-run diff).
444
+ * --scope <sys_id> Optional. sysparm_transaction_scope for the publish.
445
+ * --json Optional. Emit the structured EditFlowResult.
446
+ *
447
+ * Reads the model, applies the declarative edits, and (with --apply) re-publishes
448
+ * via the snapshot endpoint. Without --apply it prints the would-be changes.
449
+ */
450
+ async function runEditFlow(flags) {
451
+ var sysId = flags["sys-id"] || flags.sysId;
452
+ if (!sysId) {
453
+ process.stderr.write("edit-flow: --sys-id <sys_id> is required\n");
454
+ return 1;
455
+ }
456
+ if (!flags["from-json"]) {
457
+ process.stderr.write("edit-flow: --from-json <path> (EditFlowOps) is required\n");
458
+ return 1;
459
+ }
460
+ var ops = JSON.parse(fs.readFileSync(flags["from-json"], "utf8"));
461
+ var params = {
462
+ client: (0, client_1.createClient)({}),
463
+ sysId: sysId,
464
+ ops: ops,
465
+ apply: flags.apply === "true"
466
+ };
467
+ if (flags.scope || flags.scopeSysId) {
468
+ params.scopeSysId = flags.scope || flags.scopeSysId;
469
+ }
470
+ var result = await (0, editFlow_1.editFlow)(params);
471
+ if (flags.json === "true") {
472
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
473
+ return 0;
474
+ }
475
+ process.stdout.write("[" + result.status + "] " + result.changes.length + " change(s)"
476
+ + (result.snapshotSysId ? " — snapshot " + result.snapshotSysId : "") + "\n");
477
+ for (var i = 0; i < result.changes.length; i += 1) {
478
+ process.stdout.write(" + " + result.changes[i] + "\n");
479
+ }
480
+ for (var w = 0; w < result.warnings.length; w += 1) {
481
+ process.stdout.write(" ! " + result.warnings[w] + "\n");
482
+ }
483
+ return 0;
484
+ }
302
485
  async function runMcp(flags) {
303
486
  if (flags.smoke === "true") {
304
487
  await (0, server_1.runSmoke)();
@@ -324,6 +507,16 @@ function printHelp() {
324
507
  " [--view <v>] [--scope <s>] [--prune false] [--dry-run] [--json])\n" +
325
508
  " build-flow Author Custom Action Types and Subflows from a JSON spec\n" +
326
509
  " (--from-json <path> [--update-set <sys_id>] [--dry-run] [--skip-publish] [--json])\n" +
510
+ " view-flow Read a flow/subflow's compiled step graph (read-only)\n" +
511
+ " (--sys-id <sys_id> [--json] [--raw])\n" +
512
+ " view-action Read a Custom Action Type's model — inputs/outputs (read-only)\n" +
513
+ " (--sys-id <sys_id> --scope <sys_id> [--json] [--raw])\n" +
514
+ " publish-flow Compile a flow/subflow snapshot (write)\n" +
515
+ " (--sys-id <sys_id> [--scope <sys_id>] [--json])\n" +
516
+ " test-flow Validate (default) or run a flow/subflow\n" +
517
+ " (--sys-id <sys_id> [--execute --confirm] [--inputs <json>] [--json])\n" +
518
+ " edit-flow Patch a flow/subflow (rename, description, step inputs) + publish\n" +
519
+ " (--sys-id <sys_id> --from-json <ops.json> [--apply] [--scope <sys_id>] [--json])\n" +
327
520
  " mcp Run the MCP stdio server (--smoke lists tools and exits)\n");
328
521
  }
329
522
  async function main() {
@@ -335,6 +528,21 @@ async function main() {
335
528
  if (parsed.command === "build-flow") {
336
529
  return await runBuildFlowCmd(parsed.flags);
337
530
  }
531
+ if (parsed.command === "view-flow") {
532
+ return await runViewFlow(parsed.flags);
533
+ }
534
+ if (parsed.command === "view-action") {
535
+ return await runViewAction(parsed.flags);
536
+ }
537
+ if (parsed.command === "publish-flow") {
538
+ return await runPublishFlow(parsed.flags);
539
+ }
540
+ if (parsed.command === "test-flow") {
541
+ return await runTestFlow(parsed.flags);
542
+ }
543
+ if (parsed.command === "edit-flow") {
544
+ return await runEditFlow(parsed.flags);
545
+ }
338
546
  if (parsed.command === "create-view") {
339
547
  await runCreateView(parsed.flags);
340
548
  return 0;
package/dist/client.d.ts CHANGED
@@ -102,5 +102,16 @@ export interface ServiceNowClient {
102
102
  [k: string]: any;
103
103
  }>;
104
104
  };
105
+ now: {
106
+ /**
107
+ * GET an arbitrary native ServiceNow REST path (e.g. /api/now/processflow/...).
108
+ * Basic auth, same credentials/retry/throttle as the rest of the client.
109
+ * Use for endpoints that aren't the Table API or the Dovetail Scripted REST API
110
+ * — currently the Flow Designer processflow endpoints. Returns the raw response body.
111
+ */
112
+ get: <T = any>(path: string) => Promise<T>;
113
+ /** POST an arbitrary native ServiceNow REST path with a JSON body. See `get`. */
114
+ post: <T = any>(path: string, body: any) => Promise<T>;
115
+ };
105
116
  }
106
117
  export declare function createClient(config?: ServiceNowClientConfig): ServiceNowClient;
package/dist/client.js CHANGED
@@ -296,6 +296,14 @@ function createClient(config = {}) {
296
296
  var data = await dovetailRequest("POST", "deleteRecord", { table: params.table, sys_id: params.sys_id }, null, "claude.deleteRecord(" + params.table + ")");
297
297
  return data.result || data;
298
298
  }
299
+ },
300
+ now: {
301
+ get: function (path) {
302
+ return request({ method: "GET", url: path }, "now.get(" + path + ")");
303
+ },
304
+ post: function (path, body) {
305
+ return request({ method: "POST", url: path, data: body }, "now.post(" + path + ")");
306
+ }
299
307
  }
300
308
  };
301
309
  }
@@ -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,59 @@
1
+ /**
2
+ * Edit a Flow Designer flow or subflow in place, headless.
3
+ *
4
+ * Strategy: read the compiled model (GET /processflow/flow/{id}), apply a small
5
+ * set of declarative mutations to it, then re-publish it (POST .../snapshot via
6
+ * publishFlow). The publish POST persists step input values back to the design
7
+ * records — the same mechanism the Designer's Publish button uses — so a patched
8
+ * model lands in both the running snapshot and the records the Designer reads.
9
+ *
10
+ * Supported edits (intentionally narrow + safe — no structural step add/remove,
11
+ * which requires the V2 instance graph and is out of scope here):
12
+ * - rename: set name / internalName on the flow
13
+ * - description: set the flow description
14
+ * - patchStepInputs: set named input values on specific steps (by uiId or label)
15
+ *
16
+ * Anything unmatched is reported in `result.warnings` rather than silently
17
+ * dropped, so a typo'd step label or input name is visible.
18
+ *
19
+ * By default this is a DRY RUN — it computes and returns the diff WITHOUT
20
+ * publishing. Pass `apply: true` to actually publish the edited model (a write).
21
+ */
22
+ import type { ServiceNowClient } from "../client";
23
+ export interface StepInputPatch {
24
+ /** Identify the step by its uiUniqueIdentifier OR its label (name). */
25
+ step: string;
26
+ /** The input's `name` on the action instance. */
27
+ input: string;
28
+ /** New value to set (may be any JSON value, including the empty string). */
29
+ value?: any;
30
+ }
31
+ export interface EditFlowOps {
32
+ rename?: {
33
+ name?: string;
34
+ internalName?: string;
35
+ };
36
+ description?: string;
37
+ patchStepInputs?: Array<StepInputPatch>;
38
+ }
39
+ export interface EditFlowParams {
40
+ client: ServiceNowClient;
41
+ /** sys_id of the sys_hub_flow (flow or subflow) to edit. */
42
+ sysId: string;
43
+ ops: EditFlowOps;
44
+ /** When true, publish the edited model. When false/omitted, dry-run (no write). */
45
+ apply?: boolean;
46
+ /** Override sysparm_transaction_scope for the publish; defaults to the model's scope. */
47
+ scopeSysId?: string;
48
+ }
49
+ export interface EditFlowResult {
50
+ /** "dry-run" (computed, not published) or "published". */
51
+ status: "dry-run" | "published";
52
+ /** Human-readable list of changes that were applied to the model. */
53
+ changes: Array<string>;
54
+ /** Requested edits that could not be matched (e.g. unknown step or input). */
55
+ warnings: Array<string>;
56
+ /** Present when status="published". */
57
+ snapshotSysId?: string;
58
+ }
59
+ export declare function editFlow(params: EditFlowParams): Promise<EditFlowResult>;