@tenonhq/dovetail-servicenow 0.0.12 → 0.0.13

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
@@ -180,6 +180,10 @@ npx dove-sn view-action --sys-id <action_type_sys_id> --scope <scope_sys_id>
180
180
  # Copy a flow / subflow (creates an INACTIVE DRAFT) via the Designer's Copy endpoint
181
181
  npx dove-sn copy-flow --sys-id <sys_id> --name "My Copy" # scope defaults to source's
182
182
 
183
+ # Create a NEW flow (type=flow) from scratch and PUBLISH it (grafts a template's trigger+action)
184
+ npx dove-sn create-flow --name "My Flow" --template <published_flow_sys_id> --scope <scope_sys_id> \
185
+ --trigger-table customer_contact --log-message "hello" # add --dry-run to preview
186
+
183
187
  # Publish (compile the snapshot of) a flow / subflow after editing in the Designer
184
188
  npx dove-sn publish-flow --sys-id <sys_id> # scope defaults to the flow's
185
189
 
@@ -196,6 +200,21 @@ npx dove-sn edit-flow --sys-id <sys_id> --from-json ops.json --apply --update-se
196
200
  `copy-flow` calls the Designer's own `POST /processflow/flow/{id}/copy` — a
197
201
  complete, faithful clone created as an **inactive draft**. (Don't publish +
198
202
  activate a copy of a triggered production flow unless you intend it to fire.)
203
+
204
+ `create-flow` mints a **brand-new** flow (`sys_hub_flow`, `type=flow`) from scratch
205
+ and **publishes** it: `POST /processflow/flow` initialises the envelope, the
206
+ trigger + action graph is grafted from a published template flow (`--template`,
207
+ ids remapped + values patched via `--trigger-table` / `--trigger-condition` /
208
+ `--log-message`), a `versioning/create_version` bookmark is written, then the
209
+ `/snapshot` POST compiles it. The result is a **published** flow — a published
210
+ triggered flow can fire on its trigger, so do NOT graft a production send template
211
+ you don't intend to fire (the `active` flag on the result reports whether it's
212
+ live). The
213
+ `POST /processflow/flow` create step is the crux: a Table-API / Dovetail
214
+ `createRecord` insert never initialises the processflow envelope, so its snapshot
215
+ POST silently no-ops (stays draft). Sequence reverse-engineered from Workflow
216
+ Studio HARs and validated live (2026-06-02, tenonworkstudio). Exit `0` published
217
+ or dry-run; `2` created-but-not-published.
199
218
  `test-flow` defaults to **validate** — a safe pre-flight (published? inputs match
200
219
  declared variables?) that never runs the flow; `--execute --confirm` runs it via
201
220
  the server-side FlowAPI runner (deploy `resources/runFlow.md` first).
@@ -258,7 +277,8 @@ Claude Code and agents: `create_view`, `set_list_layout`, `set_form_layout`,
258
277
  `set_related_lists`, `add_choices_to_field`, plus the Flow Designer tools
259
278
  `flow_view` (read a flow/subflow's step graph), `action_view` (read an action
260
279
  type's model), `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
261
- (copy a flow as an inactive draft), `flow_test` (validate or run a flow), and
280
+ (copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
281
+ publish, grafting a template), `flow_test` (validate or run a flow), and
262
282
  `flow_edit` (patch a flow). It reads ServiceNow credentials from the same env
263
283
  vars as the CLI.
264
284
 
package/dist/cli.js CHANGED
@@ -73,6 +73,7 @@ const readFlow_1 = require("./flowDesigner/readFlow");
73
73
  const readActionType_1 = require("./flowDesigner/readActionType");
74
74
  const publishFlow_1 = require("./flowDesigner/publishFlow");
75
75
  const copyFlow_1 = require("./flowDesigner/copyFlow");
76
+ const createFlow_1 = require("./flowDesigner/createFlow");
76
77
  const editFlow_1 = require("./flowDesigner/editFlow");
77
78
  const testFlow_1 = require("./flowDesigner/testFlow");
78
79
  const flowDesigner_formatter_2 = require("./flowDesigner-formatter");
@@ -424,6 +425,79 @@ async function runCopyFlow(flags) {
424
425
  + ") — inactive draft. Publish with: dove-sn publish-flow --sys-id " + result.sysId + "\n");
425
426
  return 0;
426
427
  }
428
+ /**
429
+ * dove-sn create-flow:
430
+ * --name <name> Required. Name for the new flow.
431
+ * --template <sys_id> Required. Published sys_hub_flow whose trigger+action graph is grafted.
432
+ * --scope <sys_id> Required. Target scope (sysparm_transaction_scope).
433
+ * --internal-name <name> Optional. internal_name (defaults to a slug of --name).
434
+ * --description <text> Optional.
435
+ * --trigger-table <table> Optional. Patch the trigger's table input (e.g. customer_contact).
436
+ * --trigger-condition <q> Optional. Patch the trigger's condition (encoded query).
437
+ * --log-message <text> Optional. Patch the action's message / short_description.
438
+ * --dry-run Optional. Print the plan + template graph counts; write nothing.
439
+ * --json Optional. Emit the structured CreateFlowResult.
440
+ *
441
+ * Creates a NEW flow from scratch and PUBLISHES it: POST /processflow/flow mints an
442
+ * initialised envelope, the template's trigger+action graph is grafted on (ids remapped,
443
+ * values patched), then the snapshot is compiled. The result is a published flow — a
444
+ * published triggered flow can fire on its trigger, so do NOT graft a production send
445
+ * template you don't intend to fire (the result's `active` flag reports whether it's live).
446
+ *
447
+ * Exit codes: 0 published OR dry-run; 2 created-but-not-published (snapshot didn't compile).
448
+ */
449
+ async function runCreateFlow(flags) {
450
+ var name = flags.name;
451
+ var templateSysId = flags.template || flags["template-sys-id"] || flags.templateSysId;
452
+ var scope = flags.scope || flags.scopeSysId;
453
+ if (!name || !templateSysId || !scope) {
454
+ process.stderr.write("create-flow: --name, --template <sys_id> and --scope <sys_id> are required\n");
455
+ return 1;
456
+ }
457
+ var params = {
458
+ client: (0, client_1.createClient)({}),
459
+ name: name,
460
+ templateSysId: templateSysId,
461
+ scopeSysId: scope
462
+ };
463
+ if (flags["internal-name"] || flags.internalName) {
464
+ params.internalName = flags["internal-name"] || flags.internalName;
465
+ }
466
+ if (flags.description) {
467
+ params.description = flags.description;
468
+ }
469
+ if (flags["trigger-table"] || flags.triggerTable) {
470
+ params.triggerTable = flags["trigger-table"] || flags.triggerTable;
471
+ }
472
+ if (flags["trigger-condition"] !== undefined || flags.triggerCondition !== undefined) {
473
+ params.triggerCondition = flags["trigger-condition"] !== undefined ? flags["trigger-condition"] : flags.triggerCondition;
474
+ }
475
+ if (flags["log-message"] || flags.logMessage) {
476
+ params.logMessage = flags["log-message"] || flags.logMessage;
477
+ }
478
+ if (flags["dry-run"] === "true") {
479
+ params.dryRun = true;
480
+ }
481
+ var result = await (0, createFlow_1.createFlow)(params);
482
+ if (flags.json === "true") {
483
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
484
+ }
485
+ else if (result.status === "dry-run") {
486
+ process.stdout.write("[dry-run] would create '" + result.name + "' (internal " + result.internalName + ") in scope "
487
+ + result.scopeSysId + " — grafting " + result.graph.triggers + " trigger + "
488
+ + result.graph.actions + " action + " + result.graph.logic + " logic from template\n");
489
+ }
490
+ else {
491
+ process.stdout.write("[" + result.status + "] '" + result.name + "' sys_id " + result.sysId
492
+ + (result.snapshotSysId ? " — snapshot " + result.snapshotSysId : "")
493
+ + (result.active === undefined ? "" : result.active ? " — ACTIVE (will fire)" : " — inactive")
494
+ + "\n");
495
+ }
496
+ if (result.status === "not-published") {
497
+ return 2;
498
+ }
499
+ return 0;
500
+ }
427
501
  /**
428
502
  * dove-sn test-flow:
429
503
  * --sys-id <sys_id> Required. sys_hub_flow sys_id (flow or subflow).
@@ -554,6 +628,10 @@ function printHelp() {
554
628
  " (--sys-id <sys_id> [--scope <sys_id>] [--json])\n" +
555
629
  " copy-flow Copy a flow/subflow (inactive draft) via the Designer Copy API\n" +
556
630
  " (--sys-id <sys_id> --name <name> [--scope <sys_id>] [--json])\n" +
631
+ " create-flow Create a NEW flow (type=flow) from scratch + publish (grafts a template)\n" +
632
+ " (--name <n> --template <sys_id> --scope <sys_id>\n" +
633
+ " [--trigger-table <t>] [--trigger-condition <q>] [--log-message <m>]\n" +
634
+ " [--internal-name <n>] [--description <d>] [--dry-run] [--json])\n" +
557
635
  " test-flow Validate (default) or run a flow/subflow\n" +
558
636
  " (--sys-id <sys_id> [--execute --confirm] [--inputs <json>] [--json])\n" +
559
637
  " edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
@@ -588,6 +666,9 @@ async function main() {
588
666
  if (parsed.command === "copy-flow") {
589
667
  return await runCopyFlow(parsed.flags);
590
668
  }
669
+ if (parsed.command === "create-flow") {
670
+ return await runCreateFlow(parsed.flags);
671
+ }
591
672
  if (parsed.command === "test-flow") {
592
673
  return await runTestFlow(parsed.flags);
593
674
  }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Create a whole Flow Designer FLOW (sys_hub_flow, type=flow) from scratch and
3
+ * publish it — headless, basic auth. The missing peer of copyFlow / publishFlow:
4
+ * copyFlow duplicates an existing flow; createFlow mints a NEW flow whose trigger
5
+ * + action graph is grafted from a template, then compiled.
6
+ *
7
+ * === CANONICAL SEQUENCE (reverse-engineered from Workflow Studio HARs, 2026-06-02) ===
8
+ * The Designer SPA holds the entire flow graph client-side and persists + compiles
9
+ * it in ONE shot at publish. There are NO incremental add-trigger / add-action REST
10
+ * calls — only version bookmarks. The proven, validated sequence is:
11
+ *
12
+ * 1. CREATE POST /api/now/processflow/flow?param_only_properties=true&sysparm_transaction_scope={scope}
13
+ * body = a small properties envelope. -> 200, result.data = a FULLY INITIALISED
14
+ * empty model (server-minted id, internalName, flowCatalogVariableModelId,
15
+ * version, authoredOnReleaseVersion). This initialisation is the piece a
16
+ * Dovetail createRecord / Table API insert never produces — and without it the
17
+ * snapshot POST silently no-ops (flow stays draft, no snapshot).
18
+ * 2. GRAFT GET /api/now/processflow/flow/{template}?sysparm_transaction_scope={scope}
19
+ * -> result.data carries the template's triggerInstances / actionInstances /
20
+ * flowLogicInstances. Build the publish model = the NEW envelope with those arrays
21
+ * grafted in, ids remapped (flowSysId -> new id; fresh id + uiUniqueIdentifier),
22
+ * and values patched (trigger table/condition, action message inputs).
23
+ * 3. VERSION POST /api/now/processflow/versioning/create_version?...
24
+ * body { item_sys_id: newFlow, type: "Activate/Publish", annotation: "", favorite: false }
25
+ * 4. PUBLISH POST /api/now/processflow/flow/{newFlow}/snapshot?... body = the FLAT model
26
+ * -> result.data.status="published", isPublished=true, latestSnapshot=<sys_id>.
27
+ *
28
+ * Validated live 2026-06-02 on tenonworkstudio (x_cadso_automate): flow
29
+ * aceb8e683395cb147b18bc534d5c7b5e published with a compiled snapshot
30
+ * (7ceb02a83395cb147b18bc534d5c7b03), 1 trigger in sys_hub_trigger_instance_v2,
31
+ * 1 action in sys_hub_action_instance_v2 — zero UI clicks.
32
+ *
33
+ * The clone inherits the template's trigger + action SHAPE; author a different
34
+ * shape by building that flow once in the Designer and pointing templateSysId at it.
35
+ */
36
+ import type { ServiceNowClient } from "../client";
37
+ export interface CreateFlowParams {
38
+ client: ServiceNowClient;
39
+ /** Name for the new flow. */
40
+ name: string;
41
+ /**
42
+ * sys_id of an existing published sys_hub_flow whose trigger + action graph is
43
+ * grafted onto the new flow. Required — the trigger/action shape comes from here.
44
+ */
45
+ templateSysId: string;
46
+ /** Target application scope sys_id (sysparm_transaction_scope). Required. */
47
+ scopeSysId: string;
48
+ /** internal_name; defaults to a slug of name. */
49
+ internalName?: string;
50
+ /** description on the new flow. */
51
+ description?: string;
52
+ /** Patch the trigger's `table` input (e.g. "customer_contact"). */
53
+ triggerTable?: string;
54
+ /** Patch the trigger's `condition` input (encoded query). Pass "" to clear. */
55
+ triggerCondition?: string;
56
+ /** Patch the action's `message` input (Log message) / short_description data. */
57
+ logMessage?: string;
58
+ /**
59
+ * When true, do everything up to but not including the writes and return the
60
+ * planned new name / internalName / template-graph counts. No network writes.
61
+ */
62
+ dryRun?: boolean;
63
+ }
64
+ export interface CreateFlowResult {
65
+ status: "published" | "dry-run" | "not-published";
66
+ /** sys_id of the new flow (empty on dry-run). */
67
+ sysId: string;
68
+ name: string;
69
+ internalName: string;
70
+ scopeSysId: string;
71
+ /** sys_id of the compiled snapshot, when published. */
72
+ snapshotSysId?: string;
73
+ /** Counts grafted from the template. */
74
+ graph: {
75
+ triggers: number;
76
+ actions: number;
77
+ logic: number;
78
+ };
79
+ /**
80
+ * Whether the published flow is ACTIVE (will fire on its trigger). Read back
81
+ * from the snapshot response when it reports it; `undefined` when the response
82
+ * doesn't carry an active flag (dry-run, or a server payload that omits it).
83
+ * Don't assume inactive on `undefined` — confirm against the flow record.
84
+ */
85
+ active?: boolean;
86
+ /** HTTP status of the snapshot POST (0 on dry-run). */
87
+ httpStatus: number;
88
+ }
89
+ /**
90
+ * Graft the template's graph onto the freshly-created envelope and patch values.
91
+ * Exported for unit testing the pure transform without any network.
92
+ */
93
+ export declare function buildPublishModel(envelope: any, template: any, newFlow: string, params: CreateFlowParams): any;
94
+ export declare function createFlow(params: CreateFlowParams): Promise<CreateFlowResult>;
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ /**
3
+ * Create a whole Flow Designer FLOW (sys_hub_flow, type=flow) from scratch and
4
+ * publish it — headless, basic auth. The missing peer of copyFlow / publishFlow:
5
+ * copyFlow duplicates an existing flow; createFlow mints a NEW flow whose trigger
6
+ * + action graph is grafted from a template, then compiled.
7
+ *
8
+ * === CANONICAL SEQUENCE (reverse-engineered from Workflow Studio HARs, 2026-06-02) ===
9
+ * The Designer SPA holds the entire flow graph client-side and persists + compiles
10
+ * it in ONE shot at publish. There are NO incremental add-trigger / add-action REST
11
+ * calls — only version bookmarks. The proven, validated sequence is:
12
+ *
13
+ * 1. CREATE POST /api/now/processflow/flow?param_only_properties=true&sysparm_transaction_scope={scope}
14
+ * body = a small properties envelope. -> 200, result.data = a FULLY INITIALISED
15
+ * empty model (server-minted id, internalName, flowCatalogVariableModelId,
16
+ * version, authoredOnReleaseVersion). This initialisation is the piece a
17
+ * Dovetail createRecord / Table API insert never produces — and without it the
18
+ * snapshot POST silently no-ops (flow stays draft, no snapshot).
19
+ * 2. GRAFT GET /api/now/processflow/flow/{template}?sysparm_transaction_scope={scope}
20
+ * -> result.data carries the template's triggerInstances / actionInstances /
21
+ * flowLogicInstances. Build the publish model = the NEW envelope with those arrays
22
+ * grafted in, ids remapped (flowSysId -> new id; fresh id + uiUniqueIdentifier),
23
+ * and values patched (trigger table/condition, action message inputs).
24
+ * 3. VERSION POST /api/now/processflow/versioning/create_version?...
25
+ * body { item_sys_id: newFlow, type: "Activate/Publish", annotation: "", favorite: false }
26
+ * 4. PUBLISH POST /api/now/processflow/flow/{newFlow}/snapshot?... body = the FLAT model
27
+ * -> result.data.status="published", isPublished=true, latestSnapshot=<sys_id>.
28
+ *
29
+ * Validated live 2026-06-02 on tenonworkstudio (x_cadso_automate): flow
30
+ * aceb8e683395cb147b18bc534d5c7b5e published with a compiled snapshot
31
+ * (7ceb02a83395cb147b18bc534d5c7b03), 1 trigger in sys_hub_trigger_instance_v2,
32
+ * 1 action in sys_hub_action_instance_v2 — zero UI clicks.
33
+ *
34
+ * The clone inherits the template's trigger + action SHAPE; author a different
35
+ * shape by building that flow once in the Designer and pointing templateSysId at it.
36
+ */
37
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
38
+ if (k2 === undefined) k2 = k;
39
+ var desc = Object.getOwnPropertyDescriptor(m, k);
40
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
41
+ desc = { enumerable: true, get: function() { return m[k]; } };
42
+ }
43
+ Object.defineProperty(o, k2, desc);
44
+ }) : (function(o, m, k, k2) {
45
+ if (k2 === undefined) k2 = k;
46
+ o[k2] = m[k];
47
+ }));
48
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
49
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
50
+ }) : function(o, v) {
51
+ o["default"] = v;
52
+ });
53
+ var __importStar = (this && this.__importStar) || (function () {
54
+ var ownKeys = function(o) {
55
+ ownKeys = Object.getOwnPropertyNames || function (o) {
56
+ var ar = [];
57
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
58
+ return ar;
59
+ };
60
+ return ownKeys(o);
61
+ };
62
+ return function (mod) {
63
+ if (mod && mod.__esModule) return mod;
64
+ var result = {};
65
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
66
+ __setModuleDefault(result, mod);
67
+ return result;
68
+ };
69
+ })();
70
+ Object.defineProperty(exports, "__esModule", { value: true });
71
+ exports.buildPublishModel = buildPublishModel;
72
+ exports.createFlow = createFlow;
73
+ const crypto = __importStar(require("crypto"));
74
+ function newId() {
75
+ return crypto.randomBytes(16).toString("hex");
76
+ }
77
+ function uuid() {
78
+ return crypto.randomUUID();
79
+ }
80
+ function slug(s) {
81
+ // The first replace collapses every run of non-alphanumerics to a single "_",
82
+ // so consecutive underscores can never occur here — a single-char trim (no "+"
83
+ // quantifier) is sufficient and avoids the polynomial-backtracking pattern.
84
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
85
+ }
86
+ /** Normalize `{ result: { data } }` / `{ data }` / `{ result }` / bare to the model. */
87
+ function unwrapModel(data) {
88
+ if (data && typeof data === "object" && data.result && typeof data.result === "object") {
89
+ if (data.result.data && typeof data.result.data === "object") {
90
+ return data.result.data;
91
+ }
92
+ return data.result;
93
+ }
94
+ if (data && typeof data === "object" && data.data && typeof data.data === "object") {
95
+ return data.data;
96
+ }
97
+ return data;
98
+ }
99
+ function flowGetPath(sysId, scopeSysId) {
100
+ return "/api/now/processflow/flow/" + encodeURIComponent(sysId)
101
+ + "?sysparm_transaction_scope=" + encodeURIComponent(scopeSysId);
102
+ }
103
+ function flowCreatePath(scopeSysId) {
104
+ return "/api/now/processflow/flow?param_only_properties=true"
105
+ + "&sysparm_transaction_scope=" + encodeURIComponent(scopeSysId);
106
+ }
107
+ function createVersionPath(scopeSysId) {
108
+ return "/api/now/processflow/versioning/create_version"
109
+ + "?sysparm_transaction_scope=" + encodeURIComponent(scopeSysId);
110
+ }
111
+ function snapshotPath(sysId, scopeSysId) {
112
+ return "/api/now/processflow/flow/" + encodeURIComponent(sysId) + "/snapshot"
113
+ + "?sysparm_transaction_scope=" + encodeURIComponent(scopeSysId);
114
+ }
115
+ function patchInputs(inst, kv) {
116
+ if (!inst || !Array.isArray(inst.inputs))
117
+ return;
118
+ for (var i = 0; i < inst.inputs.length; i += 1) {
119
+ var inp = inst.inputs[i];
120
+ if (Object.prototype.hasOwnProperty.call(kv, inp.name)) {
121
+ inp.value = kv[inp.name];
122
+ if (Object.prototype.hasOwnProperty.call(inp, "displayValue")) {
123
+ inp.displayValue = kv[inp.name];
124
+ }
125
+ }
126
+ }
127
+ }
128
+ function remapInstances(arr, newFlow) {
129
+ if (!Array.isArray(arr))
130
+ return [];
131
+ return arr.map(function (src) {
132
+ var inst = JSON.parse(JSON.stringify(src));
133
+ if (Object.prototype.hasOwnProperty.call(inst, "flowSysId"))
134
+ inst.flowSysId = newFlow;
135
+ if (Object.prototype.hasOwnProperty.call(inst, "id"))
136
+ inst.id = newId();
137
+ if (Object.prototype.hasOwnProperty.call(inst, "uiUniqueIdentifier"))
138
+ inst.uiUniqueIdentifier = uuid();
139
+ return inst;
140
+ });
141
+ }
142
+ /**
143
+ * Graft the template's graph onto the freshly-created envelope and patch values.
144
+ * Exported for unit testing the pure transform without any network.
145
+ */
146
+ function buildPublishModel(envelope, template, newFlow, params) {
147
+ var M = JSON.parse(JSON.stringify(envelope));
148
+ M.id = newFlow;
149
+ M.status = "draft";
150
+ M.active = false;
151
+ M.latestSnapshot = "";
152
+ M.masterSnapshot = false;
153
+ M.masterSnapshotId = "";
154
+ M.isPublished = false;
155
+ M.snapshot = false;
156
+ M.jsonSnapshot = false;
157
+ M.deletedActions = [];
158
+ M.deletedTriggers = [];
159
+ M.deletedFlowLogicInstances = [];
160
+ M.triggerInstances = remapInstances(template.triggerInstances, newFlow);
161
+ M.actionInstances = remapInstances(template.actionInstances, newFlow);
162
+ M.flowLogicInstances = remapInstances(template.flowLogicInstances, newFlow);
163
+ M.subFlowInstances = remapInstances(template.subFlowInstances, newFlow);
164
+ var ti;
165
+ for (ti = 0; ti < M.triggerInstances.length; ti += 1) {
166
+ var kv = {};
167
+ if (params.triggerTable)
168
+ kv.table = params.triggerTable;
169
+ if (params.triggerCondition !== undefined)
170
+ kv.condition = params.triggerCondition;
171
+ patchInputs(M.triggerInstances[ti], kv);
172
+ }
173
+ var ai;
174
+ for (ai = 0; ai < M.actionInstances.length; ai += 1) {
175
+ var act = M.actionInstances[ai];
176
+ if (params.logMessage) {
177
+ patchInputs(act, { message: params.logMessage });
178
+ if (act.data && Object.prototype.hasOwnProperty.call(act.data, "values")) {
179
+ act.data.values = "short_description=" + params.logMessage;
180
+ }
181
+ }
182
+ }
183
+ return M;
184
+ }
185
+ function extractSnapshotSysId(body) {
186
+ if (!body || typeof body !== "object")
187
+ return undefined;
188
+ var snap = body.latestSnapshot || body.masterSnapshot
189
+ || body.latest_snapshot || body.master_snapshot || body.snapshot;
190
+ if (snap && typeof snap === "object") {
191
+ return typeof snap.sys_id === "string" ? snap.sys_id : undefined;
192
+ }
193
+ if (typeof snap === "string" && snap.length >= 32)
194
+ return snap;
195
+ return undefined;
196
+ }
197
+ async function createFlow(params) {
198
+ var client = params.client;
199
+ if (!params.name || params.name.trim().length === 0) {
200
+ throw new Error("createFlow: name is required.");
201
+ }
202
+ if (!params.templateSysId) {
203
+ throw new Error("createFlow: templateSysId is required (the trigger/action shape is grafted from it).");
204
+ }
205
+ if (!params.scopeSysId) {
206
+ throw new Error("createFlow: scopeSysId is required.");
207
+ }
208
+ var internalName = params.internalName || slug(params.name);
209
+ // 1/2 (read): pull the template graph first — also validates the template exists.
210
+ var template = unwrapModel(await client.now.get(flowGetPath(params.templateSysId, params.scopeSysId)));
211
+ if (!template || !Array.isArray(template.triggerInstances) || template.triggerInstances.length === 0) {
212
+ throw new Error("createFlow: template flow " + params.templateSysId + " has no trigger to graft.");
213
+ }
214
+ var graph = {
215
+ triggers: (template.triggerInstances || []).length,
216
+ actions: (template.actionInstances || []).length,
217
+ logic: (template.flowLogicInstances || []).length,
218
+ };
219
+ if (params.dryRun) {
220
+ return {
221
+ status: "dry-run",
222
+ sysId: "",
223
+ name: params.name,
224
+ internalName: internalName,
225
+ scopeSysId: params.scopeSysId,
226
+ graph: graph,
227
+ httpStatus: 0,
228
+ };
229
+ }
230
+ // 1 (write): create the flow — server mints the initialised envelope.
231
+ var props = {
232
+ access: "public",
233
+ description: params.description || "",
234
+ flowPriority: "MEDIUM",
235
+ name: params.name,
236
+ protection: "",
237
+ runAs: "system",
238
+ runWithRoles: { value: "", displayValue: "" },
239
+ type: "flow",
240
+ active: false,
241
+ deleted: false,
242
+ security: { can_read: true, can_write: true },
243
+ scope: params.scopeSysId,
244
+ scopeName: "",
245
+ scopeDisplayName: "",
246
+ status: "draft",
247
+ userHasRolesAssignedToFlow: true,
248
+ };
249
+ var createResp = await client.now.post(flowCreatePath(params.scopeSysId), props);
250
+ var envelope = unwrapModel(createResp);
251
+ if (!envelope || typeof envelope.id !== "string" || envelope.id.length < 32) {
252
+ throw new Error("createFlow: create did not return an initialised flow id: "
253
+ + JSON.stringify(createResp).substring(0, 300));
254
+ }
255
+ var newFlow = envelope.id;
256
+ if (envelope.internalName)
257
+ internalName = envelope.internalName;
258
+ // 2: build the publish model.
259
+ var model = buildPublishModel(envelope, template, newFlow, params);
260
+ // 3: version bookmark (Activate/Publish). Best-effort — publish is the gate.
261
+ try {
262
+ await client.now.post(createVersionPath(params.scopeSysId), {
263
+ item_sys_id: newFlow,
264
+ type: "Activate/Publish",
265
+ annotation: "",
266
+ favorite: false,
267
+ });
268
+ }
269
+ catch (err) {
270
+ // non-fatal; the snapshot POST is what compiles.
271
+ }
272
+ // 4: publish (compile the snapshot). request() throws on non-2xx.
273
+ var snapResp = await client.now.post(snapshotPath(newFlow, params.scopeSysId), model);
274
+ var snapBody = unwrapModel(snapResp);
275
+ var publishedFlag = snapBody && (snapBody.isPublished === true || snapBody.status === "published");
276
+ var snapshotSysId = extractSnapshotSysId(snapBody);
277
+ var active = snapBody && typeof snapBody.active === "boolean" ? snapBody.active : undefined;
278
+ return {
279
+ status: publishedFlag || snapshotSysId ? "published" : "not-published",
280
+ sysId: newFlow,
281
+ name: params.name,
282
+ internalName: internalName,
283
+ scopeSysId: params.scopeSysId,
284
+ snapshotSysId: snapshotSysId,
285
+ graph: graph,
286
+ active: active,
287
+ httpStatus: 200,
288
+ };
289
+ }
@@ -24,6 +24,8 @@ export { publishFlow } from "./publishFlow";
24
24
  export type { PublishFlowParams, PublishFlowResult } from "./publishFlow";
25
25
  export { copyFlow } from "./copyFlow";
26
26
  export type { CopyFlowParams, CopyFlowResult } from "./copyFlow";
27
+ export { createFlow, buildPublishModel } from "./createFlow";
28
+ export type { CreateFlowParams, CreateFlowResult } from "./createFlow";
27
29
  export { editFlow } from "./editFlow";
28
30
  export type { EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch } from "./editFlow";
29
31
  export { testFlow, DEFAULT_RUN_FLOW_PATH } from "./testFlow";
@@ -6,7 +6,7 @@
6
6
  * functions land in Phase 1.C/D.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.SYSTEM_FIELDS_TO_STRIP = exports.assertSysId = exports.applyScope = exports.stripSystemFields = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = void 0;
9
+ exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.SYSTEM_FIELDS_TO_STRIP = exports.assertSysId = exports.applyScope = exports.stripSystemFields = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = void 0;
10
10
  var listTemplates_1 = require("./listTemplates");
11
11
  Object.defineProperty(exports, "listTemplates", { enumerable: true, get: function () { return listTemplates_1.listTemplates; } });
12
12
  var verifyArtifact_1 = require("./verifyArtifact");
@@ -27,6 +27,9 @@ var publishFlow_1 = require("./publishFlow");
27
27
  Object.defineProperty(exports, "publishFlow", { enumerable: true, get: function () { return publishFlow_1.publishFlow; } });
28
28
  var copyFlow_1 = require("./copyFlow");
29
29
  Object.defineProperty(exports, "copyFlow", { enumerable: true, get: function () { return copyFlow_1.copyFlow; } });
30
+ var createFlow_1 = require("./createFlow");
31
+ Object.defineProperty(exports, "createFlow", { enumerable: true, get: function () { return createFlow_1.createFlow; } });
32
+ Object.defineProperty(exports, "buildPublishModel", { enumerable: true, get: function () { return createFlow_1.buildPublishModel; } });
30
33
  var editFlow_1 = require("./editFlow");
31
34
  Object.defineProperty(exports, "editFlow", { enumerable: true, get: function () { return editFlow_1.editFlow; } });
32
35
  var testFlow_1 = require("./testFlow");
package/dist/index.d.ts CHANGED
@@ -14,6 +14,6 @@ export { setFormLayout } from "./layout/formLayout";
14
14
  export { setRelatedLists } from "./layout/relatedLists";
15
15
  export { formatLayoutResult, formatCreateViewResult } from "./layout/formatter";
16
16
  export { sincPlugin } from "./plugin";
17
- export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, triggerPublication, publishActionType, readFlow, readActionType, publishFlow, copyFlow, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError } from "./flowDesigner";
18
- export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, ReadFlowParams, ReadFlowResult, FlowStep, FlowVariable, ReadActionTypeParams, ReadActionTypeResult, ActionIo, PublishFlowParams, PublishFlowResult, CopyFlowParams, CopyFlowResult, EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch, TestFlowParams, TestFlowResult, WriteOp, WriteOpResult } from "./flowDesigner";
17
+ export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, triggerPublication, publishActionType, readFlow, readActionType, publishFlow, copyFlow, createFlow, buildPublishModel, editFlow, testFlow, DEFAULT_RUN_FLOW_PATH, generateSysId, topoSort, executeWritePlan, WriteOrderError } from "./flowDesigner";
18
+ export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, TriggerPublicationParams, TriggerPublicationResult, PublishActionTypeParams, PublishActionTypeResult, ReadFlowParams, ReadFlowResult, FlowStep, FlowVariable, ReadActionTypeParams, ReadActionTypeResult, ActionIo, PublishFlowParams, PublishFlowResult, CopyFlowParams, CopyFlowResult, CreateFlowParams, CreateFlowResult, EditFlowParams, EditFlowResult, EditFlowOps, StepInputPatch, TestFlowParams, TestFlowResult, WriteOp, WriteOpResult } from "./flowDesigner";
19
19
  export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, DictionaryRecord, UpdateSetRecord, LayoutAction, LayoutRecordResult, LayoutResult, CreateViewParams, CreateViewResult, FormSectionSpec, SetFormLayoutParams, SetListLayoutParams, SetRelatedListsParams } from "./types";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * REST API so every change lands in the target update set and scope.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.addChoicesToField = exports.createClient = void 0;
9
+ exports.WriteOrderError = exports.executeWritePlan = exports.topoSort = exports.generateSysId = exports.DEFAULT_RUN_FLOW_PATH = exports.testFlow = exports.editFlow = exports.buildPublishModel = exports.createFlow = exports.copyFlow = exports.publishFlow = exports.readActionType = exports.readFlow = exports.publishActionType = exports.triggerPublication = exports.cloneActionType = exports.cloneSubflow = exports.verifyArtifact = exports.listTemplates = exports.sincPlugin = exports.formatCreateViewResult = exports.formatLayoutResult = exports.setRelatedLists = exports.setFormLayout = exports.setListLayout = exports.createView = exports.formatAddChoicesResult = exports.addChoicesToField = exports.createClient = void 0;
10
10
  var client_1 = require("./client");
11
11
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
12
12
  var choices_1 = require("./choices");
@@ -37,6 +37,8 @@ Object.defineProperty(exports, "readFlow", { enumerable: true, get: function ()
37
37
  Object.defineProperty(exports, "readActionType", { enumerable: true, get: function () { return flowDesigner_1.readActionType; } });
38
38
  Object.defineProperty(exports, "publishFlow", { enumerable: true, get: function () { return flowDesigner_1.publishFlow; } });
39
39
  Object.defineProperty(exports, "copyFlow", { enumerable: true, get: function () { return flowDesigner_1.copyFlow; } });
40
+ Object.defineProperty(exports, "createFlow", { enumerable: true, get: function () { return flowDesigner_1.createFlow; } });
41
+ Object.defineProperty(exports, "buildPublishModel", { enumerable: true, get: function () { return flowDesigner_1.buildPublishModel; } });
40
42
  Object.defineProperty(exports, "editFlow", { enumerable: true, get: function () { return flowDesigner_1.editFlow; } });
41
43
  Object.defineProperty(exports, "testFlow", { enumerable: true, get: function () { return flowDesigner_1.testFlow; } });
42
44
  Object.defineProperty(exports, "DEFAULT_RUN_FLOW_PATH", { enumerable: true, get: function () { return flowDesigner_1.DEFAULT_RUN_FLOW_PATH; } });
@@ -9,7 +9,7 @@
9
9
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import type { ServiceNowClient } from "../client";
12
- export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "flow_publish", "flow_copy", "flow_test", "flow_edit"];
12
+ export declare var TOOL_NAMES: readonly ["create_view", "set_list_layout", "set_form_layout", "set_related_lists", "add_choices_to_field", "flow_view", "action_view", "flow_publish", "flow_copy", "flow_create", "flow_test", "flow_edit"];
13
13
  export type ToolName = typeof TOOL_NAMES[number];
14
14
  export interface RegistryDeps {
15
15
  /** Optional client injection for tests; defaults to createClient({}). */
@@ -21,6 +21,7 @@ const readFlow_1 = require("../flowDesigner/readFlow");
21
21
  const readActionType_1 = require("../flowDesigner/readActionType");
22
22
  const publishFlow_1 = require("../flowDesigner/publishFlow");
23
23
  const copyFlow_1 = require("../flowDesigner/copyFlow");
24
+ const createFlow_1 = require("../flowDesigner/createFlow");
24
25
  const editFlow_1 = require("../flowDesigner/editFlow");
25
26
  const testFlow_1 = require("../flowDesigner/testFlow");
26
27
  const schemas_1 = require("./schemas");
@@ -34,6 +35,7 @@ exports.TOOL_NAMES = [
34
35
  "action_view",
35
36
  "flow_publish",
36
37
  "flow_copy",
38
+ "flow_create",
37
39
  "flow_test",
38
40
  "flow_edit"
39
41
  ];
@@ -141,6 +143,35 @@ function buildDescriptors(deps = {}) {
141
143
  return (0, copyFlow_1.copyFlow)({ client: client(), sourceSysId: p.sourceSysId, newName: p.newName, scopeSysId: p.scopeSysId });
142
144
  }
143
145
  },
146
+ {
147
+ name: "flow_create",
148
+ description: "Create a NEW ServiceNow Flow Designer flow (sys_hub_flow, type=flow) from scratch and "
149
+ + "PUBLISH it, headless. Mints a fresh flow via POST /processflow/flow, grafts the trigger + "
150
+ + "action graph from an existing published template flow (templateSysId), then compiles the "
151
+ + "snapshot — leaving a published flow (the result's `active` flag reports whether it will "
152
+ + "fire). Unlike flow_copy (which duplicates a flow), "
153
+ + "this creates a new flow you can re-point at a different trigger table / message. "
154
+ + "name + templateSysId + scopeSysId are required; triggerTable / triggerCondition / "
155
+ + "logMessage patch the grafted graph; dryRun:true returns the plan + template graph counts "
156
+ + "without writing. WARNING: a published triggered flow can fire on its trigger — do not graft "
157
+ + "a production send template you don't intend to fire.",
158
+ shape: schemas_1.createFlowSchema.shape,
159
+ handler: async function (args) {
160
+ var p = schemas_1.createFlowSchema.parse(args);
161
+ return (0, createFlow_1.createFlow)({
162
+ client: client(),
163
+ name: p.name,
164
+ templateSysId: p.templateSysId,
165
+ scopeSysId: p.scopeSysId,
166
+ internalName: p.internalName,
167
+ description: p.description,
168
+ triggerTable: p.triggerTable,
169
+ triggerCondition: p.triggerCondition,
170
+ logMessage: p.logMessage,
171
+ dryRun: p.dryRun
172
+ });
173
+ }
174
+ },
144
175
  {
145
176
  name: "flow_test",
146
177
  description: "Test or run a ServiceNow flow/subflow. mode='validate' (default) is a safe, read-only "
@@ -231,6 +231,37 @@ export declare var copyFlowSchema: z.ZodObject<{
231
231
  newName: string;
232
232
  scopeSysId?: string | undefined;
233
233
  }>;
234
+ export declare var createFlowSchema: z.ZodObject<{
235
+ name: z.ZodString;
236
+ templateSysId: z.ZodString;
237
+ scopeSysId: z.ZodString;
238
+ internalName: z.ZodOptional<z.ZodString>;
239
+ description: z.ZodOptional<z.ZodString>;
240
+ triggerTable: z.ZodOptional<z.ZodString>;
241
+ triggerCondition: z.ZodOptional<z.ZodString>;
242
+ logMessage: z.ZodOptional<z.ZodString>;
243
+ dryRun: z.ZodOptional<z.ZodBoolean>;
244
+ }, "strip", z.ZodTypeAny, {
245
+ name: string;
246
+ scopeSysId: string;
247
+ templateSysId: string;
248
+ description?: string | undefined;
249
+ dryRun?: boolean | undefined;
250
+ internalName?: string | undefined;
251
+ triggerTable?: string | undefined;
252
+ triggerCondition?: string | undefined;
253
+ logMessage?: string | undefined;
254
+ }, {
255
+ name: string;
256
+ scopeSysId: string;
257
+ templateSysId: string;
258
+ description?: string | undefined;
259
+ dryRun?: boolean | undefined;
260
+ internalName?: string | undefined;
261
+ triggerTable?: string | undefined;
262
+ triggerCondition?: string | undefined;
263
+ logMessage?: string | undefined;
264
+ }>;
234
265
  export declare var testFlowSchema: z.ZodObject<{
235
266
  sysId: z.ZodString;
236
267
  mode: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"validate">, z.ZodLiteral<"execute">]>>;
@@ -4,7 +4,7 @@
4
4
  * own file so registry.ts stays focused on wiring.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.editFlowSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
7
+ exports.editFlowSchema = exports.stepInputPatchSchema = exports.testFlowSchema = exports.createFlowSchema = exports.copyFlowSchema = exports.publishFlowSchema = exports.viewActionSchema = exports.viewFlowSchema = exports.addChoicesToFieldSchema = exports.choiceValueSchema = exports.setRelatedListsSchema = exports.setFormLayoutSchema = exports.formSectionSchema = exports.setListLayoutSchema = exports.createViewSchema = void 0;
8
8
  const zod_1 = require("zod");
9
9
  exports.createViewSchema = zod_1.z.object({
10
10
  name: zod_1.z.string().min(1),
@@ -76,6 +76,17 @@ exports.copyFlowSchema = zod_1.z.object({
76
76
  newName: zod_1.z.string().min(1),
77
77
  scopeSysId: zod_1.z.string().optional()
78
78
  });
79
+ exports.createFlowSchema = zod_1.z.object({
80
+ name: zod_1.z.string().min(1),
81
+ templateSysId: zod_1.z.string().min(1),
82
+ scopeSysId: zod_1.z.string().min(1),
83
+ internalName: zod_1.z.string().optional(),
84
+ description: zod_1.z.string().optional(),
85
+ triggerTable: zod_1.z.string().optional(),
86
+ triggerCondition: zod_1.z.string().optional(),
87
+ logMessage: zod_1.z.string().optional(),
88
+ dryRun: zod_1.z.boolean().optional()
89
+ });
79
90
  exports.testFlowSchema = zod_1.z.object({
80
91
  sysId: zod_1.z.string().min(1),
81
92
  mode: zod_1.z.union([zod_1.z.literal("validate"), zod_1.z.literal("execute")]).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },