@tenonhq/dovetail-servicenow 0.0.6 → 0.0.7

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
@@ -212,6 +212,53 @@ npx dove-sn mcp # run the stdio server (wire into .mcp.json)
212
212
  This server is separate from `@tenonhq/dovetail-mcp` (the read-only cross-system
213
213
  aggregator) — `dovetail-servicenow`'s server is the ServiceNow **write** surface.
214
214
 
215
+ ## Publishing a Custom Action Type
216
+
217
+ `publishActionType` compiles the `sys_hub_flow_snapshot` for a Custom Action Type
218
+ — the step that makes it draggable in the Flow Designer palette. This replays the
219
+ Designer's **Publish** button, which is a plain REST call that **works with basic
220
+ auth** (no session cookie, CSRF token, or `sn_build_agent` role):
221
+
222
+ ```
223
+ GET /api/now/processflow/action/action_types/{sysId}?sysparm_transaction_scope={scope}
224
+ -> 200, the full action-type model EXCEPT `steps` (always returns null)
225
+ POST /api/now/processflow/action/action_types/{sysId}/snapshot?sysparm_transaction_scope={scope}
226
+ body = the model with a `steps` array grafted in
227
+ -> 201 Created (compiles the snapshot; also persists step input values
228
+ back to sys_variable_value)
229
+ ```
230
+
231
+ This is the **real** snapshot compiler and supersedes `triggerPublication` for
232
+ action types — that function ships in degraded mode (it only sets
233
+ `status="published"` and polls, because the snapshot trigger was unknown when it
234
+ was written). `triggerPublication` is retained for back-compat and the subflow path.
235
+
236
+ ```ts
237
+ import { createClient, publishActionType } from "@tenonhq/dovetail-servicenow";
238
+
239
+ var client = createClient({});
240
+ var result = await publishActionType({
241
+ client: client,
242
+ sysId: "60e6743e33814bd07b18bc534d5c7b9e", // sys_hub_action_type_definition
243
+ scopeSysId: "cd61acbbc3c85a1085b196c4e40131bd", // sysparm_transaction_scope
244
+ steps: require("./fixtures/my-action.steps.json") // see caveat below
245
+ });
246
+ // { status: "published", httpStatus: 201, snapshotSysId?: "..." }
247
+ ```
248
+
249
+ ### Steps-fixture caveat (required)
250
+
251
+ The GET returns **`steps: null`** even for an already-published action — the
252
+ Designer assembles `steps` client-side from the step records. So to publish you
253
+ **must supply a `steps` fixture** via `params.steps`. Each step's `action` field
254
+ is remapped to the target `sysId` automatically. If you omit `steps` and the
255
+ fetched model has no usable `steps` array, `publishActionType` throws with a clear
256
+ message. For a faithful clone, capture the source action's `steps` from a HAR of
257
+ the Publish call and store it as a fixture beside your driver.
258
+
259
+ Full recipe and the 6-record action-type graph:
260
+ `docs/servicenow-flow-designer-headless-authoring.md` in the CTO repo.
261
+
215
262
  ## Roadmap
216
263
 
217
264
  The same query-to-diff pattern will continue across the rest of the
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
  }
@@ -14,6 +14,8 @@ export { cloneActionType } from "./cloneActionType";
14
14
  export type { CloneActionTypeParams, CloneActionTypeResult } from "./cloneActionType";
15
15
  export { triggerPublication } from "./triggerPublication";
16
16
  export type { TriggerPublicationParams, TriggerPublicationResult } from "./triggerPublication";
17
+ export { publishActionType } from "./publishActionType";
18
+ export type { PublishActionTypeParams, PublishActionTypeResult } from "./publishActionType";
17
19
  export { generateSysId, stripSystemFields, applyScope, assertSysId, SYSTEM_FIELDS_TO_STRIP, } from "./shape";
18
20
  export { topoSort, executeWritePlan, WriteOrderError } from "./writeOrder";
19
21
  export type { WriteOp, WriteOpResult } from "./writeOrder";
@@ -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.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.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");
@@ -17,6 +17,8 @@ var cloneActionType_1 = require("./cloneActionType");
17
17
  Object.defineProperty(exports, "cloneActionType", { enumerable: true, get: function () { return cloneActionType_1.cloneActionType; } });
18
18
  var triggerPublication_1 = require("./triggerPublication");
19
19
  Object.defineProperty(exports, "triggerPublication", { enumerable: true, get: function () { return triggerPublication_1.triggerPublication; } });
20
+ var publishActionType_1 = require("./publishActionType");
21
+ Object.defineProperty(exports, "publishActionType", { enumerable: true, get: function () { return publishActionType_1.publishActionType; } });
20
22
  var shape_1 = require("./shape");
21
23
  Object.defineProperty(exports, "generateSysId", { enumerable: true, get: function () { return shape_1.generateSysId; } });
22
24
  Object.defineProperty(exports, "stripSystemFields", { enumerable: true, get: function () { return shape_1.stripSystemFields; } });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Publish a Custom Action Type — the real Flow Designer snapshot compiler.
3
+ *
4
+ * This is the working path that supersedes the degraded `triggerPublication`
5
+ * (which only set status="published" and polled, with a comment admitting the
6
+ * real snapshot trigger was an unknown "Phase 0 spike"). The Designer's
7
+ * **Publish** button is a plain REST call that works with basic auth — no
8
+ * session token, CSRF, or sn_build_agent role:
9
+ *
10
+ * GET /api/now/processflow/action/action_types/{sysId}?sysparm_transaction_scope={scope}
11
+ * -> 200, the full action-type model EXCEPT `steps` (which comes back null)
12
+ * POST /api/now/processflow/action/action_types/{sysId}/snapshot?sysparm_transaction_scope={scope}
13
+ * body = the model with a `steps` array grafted in
14
+ * -> 201 Created (compiles sys_hub_flow_snapshot; sets latest/master_snapshot;
15
+ * also persists step input values back to sys_variable_value)
16
+ *
17
+ * STEPS-FIXTURE CAVEAT: the GET returns `steps: null` even for a published
18
+ * action — the Designer assembles `steps` client-side from the step records.
19
+ * So a `steps` fixture is required to publish. Supply it via `params.steps`
20
+ * (each step's `action` is remapped to `sysId`). If the model already carries
21
+ * a non-empty `steps` array, that is used instead. If neither is available,
22
+ * this throws — the caller must provide a steps fixture.
23
+ *
24
+ * Full write-up: docs/servicenow-flow-designer-headless-authoring.md.
25
+ */
26
+ import type { ServiceNowClient } from "../client";
27
+ export interface PublishActionTypeParams {
28
+ client: ServiceNowClient;
29
+ /** sys_id of the sys_hub_action_type_definition to publish. */
30
+ sysId: string;
31
+ /** Application scope sys_id, passed as sysparm_transaction_scope. */
32
+ scopeSysId: string;
33
+ /**
34
+ * The `steps` array to graft onto the model. Required when the GET returns
35
+ * `steps: null` (the normal case). Each step's `action` is remapped to `sysId`.
36
+ * Omit only when the fetched model already carries a non-empty `steps` array.
37
+ */
38
+ steps?: Array<Record<string, any>>;
39
+ }
40
+ export interface PublishActionTypeResult {
41
+ status: "published";
42
+ /** HTTP status of the snapshot POST (201 on success). */
43
+ httpStatus: number;
44
+ /** sys_id of the compiled snapshot, when the response surfaces it. */
45
+ snapshotSysId?: string;
46
+ }
47
+ export declare function publishActionType(params: PublishActionTypeParams): Promise<PublishActionTypeResult>;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ /**
3
+ * Publish a Custom Action Type — the real Flow Designer snapshot compiler.
4
+ *
5
+ * This is the working path that supersedes the degraded `triggerPublication`
6
+ * (which only set status="published" and polled, with a comment admitting the
7
+ * real snapshot trigger was an unknown "Phase 0 spike"). The Designer's
8
+ * **Publish** button is a plain REST call that works with basic auth — no
9
+ * session token, CSRF, or sn_build_agent role:
10
+ *
11
+ * GET /api/now/processflow/action/action_types/{sysId}?sysparm_transaction_scope={scope}
12
+ * -> 200, the full action-type model EXCEPT `steps` (which comes back null)
13
+ * POST /api/now/processflow/action/action_types/{sysId}/snapshot?sysparm_transaction_scope={scope}
14
+ * body = the model with a `steps` array grafted in
15
+ * -> 201 Created (compiles sys_hub_flow_snapshot; sets latest/master_snapshot;
16
+ * also persists step input values back to sys_variable_value)
17
+ *
18
+ * STEPS-FIXTURE CAVEAT: the GET returns `steps: null` even for a published
19
+ * action — the Designer assembles `steps` client-side from the step records.
20
+ * So a `steps` fixture is required to publish. Supply it via `params.steps`
21
+ * (each step's `action` is remapped to `sysId`). If the model already carries
22
+ * a non-empty `steps` array, that is used instead. If neither is available,
23
+ * this throws — the caller must provide a steps fixture.
24
+ *
25
+ * Full write-up: docs/servicenow-flow-designer-headless-authoring.md.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.publishActionType = publishActionType;
29
+ function actionTypePath(sysId, scopeSysId, suffix) {
30
+ return "/api/now/processflow/action/action_types/" + encodeURIComponent(sysId)
31
+ + suffix
32
+ + "?sysparm_transaction_scope=" + encodeURIComponent(scopeSysId);
33
+ }
34
+ /**
35
+ * Unwrap the various response envelopes ServiceNow uses. The processflow
36
+ * endpoints have been observed returning the model both bare and under
37
+ * `result`; normalize to the model object.
38
+ */
39
+ function unwrap(data) {
40
+ if (data && typeof data === "object" && data.result && typeof data.result === "object") {
41
+ return data.result;
42
+ }
43
+ return data;
44
+ }
45
+ async function publishActionType(params) {
46
+ var client = params.client;
47
+ var sysId = params.sysId;
48
+ var scopeSysId = params.scopeSysId;
49
+ if (!sysId) {
50
+ throw new Error("publishActionType: sysId is required.");
51
+ }
52
+ if (!scopeSysId) {
53
+ throw new Error("publishActionType: scopeSysId is required (sysparm_transaction_scope).");
54
+ }
55
+ // 1. GET the model. Returns everything except `steps` (always null).
56
+ var getResp = await client.now.get(actionTypePath(sysId, scopeSysId, ""));
57
+ var model = unwrap(getResp);
58
+ if (!model || typeof model !== "object") {
59
+ throw new Error("publishActionType: unexpected GET response for action type " + sysId
60
+ + " — expected a model object, got: " + JSON.stringify(getResp).substring(0, 300));
61
+ }
62
+ // 2. Resolve the steps to graft. Caller-supplied steps win; otherwise reuse
63
+ // a non-empty steps array already on the model. The GET returns steps:null,
64
+ // so in practice a steps fixture must be supplied — fail loudly if absent.
65
+ var steps = null;
66
+ if (params.steps && params.steps.length > 0) {
67
+ steps = params.steps.map(function (step) {
68
+ var copy = {};
69
+ for (var key in step) {
70
+ if (Object.prototype.hasOwnProperty.call(step, key)) {
71
+ copy[key] = step[key];
72
+ }
73
+ }
74
+ copy.action = sysId;
75
+ return copy;
76
+ });
77
+ }
78
+ else if (model.steps && Array.isArray(model.steps) && model.steps.length > 0) {
79
+ steps = model.steps;
80
+ }
81
+ if (!steps || steps.length === 0) {
82
+ throw new Error("publishActionType: no steps to publish for action type " + sysId + ". "
83
+ + "The GET returns steps:null, so you must supply a steps fixture via params.steps. "
84
+ + "See docs/servicenow-flow-designer-headless-authoring.md.");
85
+ }
86
+ model.steps = steps;
87
+ // 3. POST the grafted model to /snapshot — compiles the snapshot (201).
88
+ var snapResp = await client.now.post(actionTypePath(sysId, scopeSysId, "/snapshot"), model);
89
+ var snapBody = unwrap(snapResp);
90
+ // request() throws on any non-2xx, so reaching here means a 2xx (201 in practice).
91
+ var snapshotSysId;
92
+ if (snapBody && typeof snapBody === "object") {
93
+ // The snapshot ref is surfaced under different keys and as either a bare
94
+ // sys_id string or a record object — coerce whichever is present to a string.
95
+ var snap = snapBody.latest_snapshot || snapBody.master_snapshot || snapBody.snapshot;
96
+ if (snap && typeof snap === "object") {
97
+ snapshotSysId = typeof snap.sys_id === "string" ? snap.sys_id : undefined;
98
+ }
99
+ else if (typeof snap === "string") {
100
+ snapshotSysId = snap;
101
+ }
102
+ }
103
+ return {
104
+ status: "published",
105
+ httpStatus: 201,
106
+ snapshotSysId: snapshotSysId
107
+ };
108
+ }
@@ -1,10 +1,18 @@
1
1
  /**
2
2
  * Best-effort publication trigger for a Subflow or Custom Action Type.
3
3
  *
4
- * Ships in DEGRADED MODE for Phase 1 the deterministic field/value combo
5
- * that compels ServiceNow to compile sys_hub_flow_snapshot is the subject
6
- * of Phase 0's gating spike and is not yet known. Until that lands, this
7
- * function:
4
+ * DEPRECATED FOR ACTION TYPES the real snapshot compiler is now known and
5
+ * shipped as `publishActionType` (see ./publishActionType.ts). That function
6
+ * replays the Flow Designer Publish call (GET model graft `steps` POST to
7
+ * /snapshot → 201) over basic auth and actually compiles sys_hub_flow_snapshot,
8
+ * which is what makes a Custom Action Type draggable in the palette. Prefer
9
+ * `publishActionType` for action types. `triggerPublication` is retained for
10
+ * back-compat and for the subflow path, which still uses the degraded trigger
11
+ * below.
12
+ *
13
+ * Ships in DEGRADED MODE — the deterministic field/value combo that compels
14
+ * ServiceNow to compile sys_hub_flow_snapshot was, when this was written, an
15
+ * unknown "Phase 0 spike". This function:
8
16
  *
9
17
  * 1. Sets `status="published"` on the parent record via pushWithUpdateSet
10
18
  * (this is the most likely single-field trigger; harmless if it isn't).
@@ -2,10 +2,18 @@
2
2
  /**
3
3
  * Best-effort publication trigger for a Subflow or Custom Action Type.
4
4
  *
5
- * Ships in DEGRADED MODE for Phase 1 the deterministic field/value combo
6
- * that compels ServiceNow to compile sys_hub_flow_snapshot is the subject
7
- * of Phase 0's gating spike and is not yet known. Until that lands, this
8
- * function:
5
+ * DEPRECATED FOR ACTION TYPES the real snapshot compiler is now known and
6
+ * shipped as `publishActionType` (see ./publishActionType.ts). That function
7
+ * replays the Flow Designer Publish call (GET model graft `steps` POST to
8
+ * /snapshot → 201) over basic auth and actually compiles sys_hub_flow_snapshot,
9
+ * which is what makes a Custom Action Type draggable in the palette. Prefer
10
+ * `publishActionType` for action types. `triggerPublication` is retained for
11
+ * back-compat and for the subflow path, which still uses the degraded trigger
12
+ * below.
13
+ *
14
+ * Ships in DEGRADED MODE — the deterministic field/value combo that compels
15
+ * ServiceNow to compile sys_hub_flow_snapshot was, when this was written, an
16
+ * unknown "Phase 0 spike". This function:
9
17
  *
10
18
  * 1. Sets `status="published"` on the parent record via pushWithUpdateSet
11
19
  * (this is the most likely single-field trigger; harmless if it isn't).
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, generateSysId, topoSort, executeWritePlan, WriteOrderError } from "./flowDesigner";
18
- export type { TemplateRef, ListTemplatesParams, FlowKind, VerifyExpect, VerifyFound, VerifyFailure, VerifyReport, VerifyArtifactParams, CloneSubflowParams, CloneSubflowResult, CloneActionTypeParams, CloneActionTypeResult, TriggerPublicationParams, TriggerPublicationResult, WriteOp, WriteOpResult } from "./flowDesigner";
17
+ export { listTemplates, verifyArtifact, cloneSubflow, cloneActionType, triggerPublication, publishActionType, 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, 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.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.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");
@@ -32,6 +32,7 @@ Object.defineProperty(exports, "verifyArtifact", { enumerable: true, get: functi
32
32
  Object.defineProperty(exports, "cloneSubflow", { enumerable: true, get: function () { return flowDesigner_1.cloneSubflow; } });
33
33
  Object.defineProperty(exports, "cloneActionType", { enumerable: true, get: function () { return flowDesigner_1.cloneActionType; } });
34
34
  Object.defineProperty(exports, "triggerPublication", { enumerable: true, get: function () { return flowDesigner_1.triggerPublication; } });
35
+ Object.defineProperty(exports, "publishActionType", { enumerable: true, get: function () { return flowDesigner_1.publishActionType; } });
35
36
  Object.defineProperty(exports, "generateSysId", { enumerable: true, get: function () { return flowDesigner_1.generateSysId; } });
36
37
  Object.defineProperty(exports, "topoSort", { enumerable: true, get: function () { return flowDesigner_1.topoSort; } });
37
38
  Object.defineProperty(exports, "executeWritePlan", { enumerable: true, get: function () { return flowDesigner_1.executeWritePlan; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenonhq/dovetail-servicenow",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },