@tenonhq/dovetail-servicenow 0.0.42 → 0.0.44

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
@@ -299,6 +299,11 @@ npx dove-sn clone-action --from <source_sys_id> --name "Send REST (Spoke)" \
299
299
  --scope x_cadso_email_spok --ops ops.json # dry-run (plan)
300
300
  npx dove-sn clone-action --from <source_sys_id> --name "Send REST (Spoke)" \
301
301
  --scope x_cadso_email_spok --ops ops.json --update-set <id> --confirm # write + publish + verify
302
+
303
+ # Define an action type's inputs, outputs and steps (script + REST, pill-wired) like the Designer's Save
304
+ npx dove-sn define-action --sys-id <id> --scope x_cadso_email_spok --spec spec.json # dry-run (diff)
305
+ npx dove-sn define-action --sys-id <id> --scope x_cadso_email_spok --spec spec.json \
306
+ --update-set <id> --confirm --publish # save + verify + publish
302
307
  ```
303
308
 
304
309
  ### Cloning an action type (`clone-action` / `action_clone`)
@@ -354,6 +359,150 @@ The MCP tool **`action_clone`** takes the same inputs — `from`, `name`, `scope
354
359
  `dryRun` — with the same dry-run-unless-`confirm:true` gate. `setStepInputs` is also
355
360
  accepted by `edit-action` / `action_edit`.
356
361
 
362
+ ### Defining an action type's body (`define-action` / `action_define`)
363
+
364
+ `define-action` authors a Custom Action Type's **action inputs, outputs and steps** —
365
+ script steps and REST steps, wired together with data pills — headlessly, the way
366
+ the Flow Designer's **Save** button does, and optionally publishes it.
367
+
368
+ ```bash
369
+ npx dove-sn define-action --sys-id <action_sys_id> --scope x_cadso_email_spok \
370
+ --spec spec.json # dry-run: the planned diff
371
+ npx dove-sn define-action --sys-id <action_sys_id> --scope x_cadso_email_spok \
372
+ --spec spec.json --update-set <id> --confirm # save + verify
373
+ npx dove-sn define-action --sys-id <action_sys_id> --scope x_cadso_email_spok \
374
+ --spec spec.json --update-set <id> --confirm --publish # save + verify + publish
375
+ ```
376
+
377
+ How it works (established from two captured Designer saves):
378
+
379
+ 1. `GET /api/now/processflow/action/action_types/{id}` — the model (43 keys; `steps` is null).
380
+ 2. `GET …/{id}/step_instances` — the real step graph.
381
+ 3. Merge the spec. Existing steps keep their `cid`; new steps are built from the
382
+ Designer's own step shape for that type (script / REST) with a fresh `cid`.
383
+ 4. `PUT …/{id}` with the **full** model — the Designer's save is not a delta. Each
384
+ step is sent in the Designer's 11-key shape (`DB_TYPE`, `cid`, `step_type_id`,
385
+ `section`, `label`, `action`, `order`, `inputs`, `extended_inputs`,
386
+ `extended_outputs`, `error_handling_type`).
387
+ 5. Read the model + steps back and compare them with the plan (a mismatch exits `1`).
388
+ 6. `--publish`: snapshot through the existing `publishActionType` path.
389
+
390
+ **Dry-run by default** — without `--confirm` it prints the planned diff (inputs,
391
+ outputs and steps added / changed / removed, with each step's input values) and makes
392
+ no write. **Idempotent** — a spec that is already in effect is `unchanged` and makes no
393
+ PUT. `--update-set` is optional; when given, the REST session is pinned to it before the
394
+ save and the publish. Exit `0` on success (incl. dry-run / unchanged), `1` on error or a
395
+ failed verify.
396
+
397
+ **The action shell must already exist.** Creating a brand-new empty action headlessly is
398
+ out of scope: make it with `clone-action` or in the Designer, then define its body here.
399
+
400
+ #### Spec
401
+
402
+ Every part is optional, so a spec can be a small incremental edit.
403
+
404
+ | Key | Shape | Notes |
405
+ |---|---|---|
406
+ | `action` | `{ name?, description?, access?: "public" \| "package_private" }` | `name` sets `name` + `displayName` |
407
+ | `inputs[]` | `{ name, label?, type?, mandatory?, choices?: [{value, label?}], default?, order?, maxLength?, remove? }` | Upsert by `name`. `type`: `string` (default) \| `choice` \| `boolean` \| `integer`. `choice` needs `choices`; `default` must be one of them. Changing an input's type makes ServiceNow mint a new variable record |
408
+ | `outputs[]` | `{ name, label?, type?, value?, remove? }` | Upsert by `name`. `value` is the pill the output is wired to. System outputs (`__action_status__`, `__dont_treat_as_error__`) cannot be named |
409
+ | `steps[]` | `{ ref, type: "script" \| "rest", label?, match?, remove?, errorHandling?, script?, inputs?, outputs?, values? }` | See below |
410
+
411
+ Steps:
412
+
413
+ - **Matching.** `match` (an existing step's cid or current label — use it to rename),
414
+ else `label`, else position (the spec's Nth step against the action's Nth step, same
415
+ type, flagged in the diff as `matched by order`). Unmatched steps are created and
416
+ placed before the next existing step the spec lists after them (else appended).
417
+ Existing steps are never reordered; `remove: true` deletes one.
418
+ - **`script`** (script steps) — the script body. On the CLI, `scriptFile` (relative
419
+ to the spec file) is sugar for it.
420
+ - **`inputs`** (script steps) — the step's own input variables (`extended_inputs`):
421
+ `{ "<name>": "<value or pill>" }` or `{ "<name>": { value?, type?, label?, mandatory?, remove? } }`.
422
+ - **`outputs`** (script steps) — the step's own output variables (`extended_outputs`):
423
+ `[{ name, label?, type?, remove? }]`.
424
+ - **`values`** — the step type's own inputs by name (unknown names are an error that
425
+ lists the valid ones). For a REST step: `connection` (`use_connection_alias`),
426
+ `connection_alias` (a `sys_alias` sys_id — its display name is looked up — or
427
+ `{ value, display }`), `override_base_url`, `base_url`, `resource_path`,
428
+ `http_method` (`get` / `post` / `put` / `delete` …), `headers` and `query_params`
429
+ (`[{ name, value }]` → the Designer's `ADV_NV` list), `body`, `request_type`,
430
+ `connection_timeout`, `retry_policy`, … Booleans take `true` / `false`.
431
+ - **`errorHandling`** — `EVAL_ERRORS` (the default) or `NEXT_STEP` (continue on error).
432
+
433
+ Pills (in `values`, script `inputs` and `outputs[].value`):
434
+
435
+ | Pill | Means |
436
+ |---|---|
437
+ | `{{action.<input>}}` | an action input — must exist after the merge |
438
+ | `{{steps.<ref>.<output>}}` | another spec step's output — resolved to `{{step[<cid>].<output>}}`. Inside a step it must point at an **earlier** step |
439
+ | `{{step[<cid>].<output>}}` | the raw form — the cid must exist |
440
+
441
+ REST steps expose `status_code`, `response_body`, `response_headers`, `error_message`,
442
+ `error_code`, `response_stream`; a script step exposes its `outputs`. An **action
443
+ output is wired by putting the pill in the output's own `value`** (the Designer also
444
+ mirrors it into `display_value` and records the pill's label in `label_cache` — both
445
+ handled for you). Everything is checked before any write: unknown keys, names with `^`
446
+ or path characters (names must match `^[A-Za-z_][A-Za-z0-9_]*$`), an unknown step type,
447
+ an unknown step ref / action input / step output in a pill, and any pill the spec would
448
+ leave dangling (e.g. removing an input a step still reads).
449
+
450
+ Example — the complete body of *Email Service Request GET*:
451
+
452
+ ```json
453
+ {
454
+ "inputs": [
455
+ { "name": "host", "type": "choice", "mandatory": true, "default": "api",
456
+ "choices": [
457
+ { "value": "api", "label": "API" },
458
+ { "value": "storage_us_east4", "label": "US East 4" },
459
+ { "value": "storage_us_west1", "label": "US West 1" },
460
+ { "value": "storage_europe_west1", "label": "Europe West 1" }
461
+ ] },
462
+ { "name": "path", "type": "string", "mandatory": true },
463
+ { "name": "content_type", "type": "string", "mandatory": false },
464
+ { "name": "body", "type": "string", "mandatory": false }
465
+ ],
466
+ "steps": [
467
+ { "ref": "guard", "type": "script", "label": "Guard", "match": "Gaurd",
468
+ "scriptFile": "guard.js",
469
+ "inputs": {
470
+ "host_1": { "value": "{{action.host}}", "mandatory": true },
471
+ "path_1": { "value": "{{action.path}}", "mandatory": true }
472
+ },
473
+ "outputs": [
474
+ { "name": "base_url", "label": "Base URL", "type": "string" },
475
+ { "name": "error", "label": "Error", "type": "string" }
476
+ ] },
477
+ { "ref": "call", "type": "rest", "label": "Call email service", "match": "REST step",
478
+ "errorHandling": "NEXT_STEP",
479
+ "values": {
480
+ "connection": "use_connection_alias",
481
+ "connection_alias": "956cc622c3ee4a1085b196c4e401317e",
482
+ "override_base_url": true,
483
+ "base_url": "{{steps.guard.base_url}}",
484
+ "resource_path": "{{action.path}}",
485
+ "http_method": "get",
486
+ "headers": [{ "name": "Content-Type", "value": "{{action.content_type}}" }],
487
+ "connection_timeout": "25000"
488
+ } }
489
+ ],
490
+ "outputs": [
491
+ { "name": "status_code", "label": "Status Code", "type": "string", "value": "{{steps.call.status_code}}" },
492
+ { "name": "response_body", "label": "Response Body", "type": "string", "value": "{{steps.call.response_body}}" },
493
+ { "name": "error", "label": "Error", "type": "string", "value": "{{steps.call.error_message}}" }
494
+ ]
495
+ }
496
+ ```
497
+
498
+ The MCP tool **`action_define`** takes `sysId`, `scope`, `spec` (inline — `script`, not
499
+ `scriptFile`), `updateSetSysId`, `publish`, `confirm`, `dryRun`, with the same
500
+ dry-run-unless-`confirm:true` gate.
501
+
502
+ Not yet covered (no Designer capture of the shape yet): step types other than script
503
+ and REST, reference / object / array variable types, and the action's error-status
504
+ conditions (`action_status_metadata`, which is carried through unchanged).
505
+
357
506
  ### Editing an action type's steps (`--from-json`)
358
507
 
359
508
  The flag form above patches the one auto-detected script. When you need to touch
@@ -813,6 +962,9 @@ type's model), `action_edit` (structurally edit a published action type — per-
813
962
  scripts, step-level inputs/outputs, data-pill wiring — dry-run by default, and the
814
963
  publish is read back and verified), `action_clone` (clone an action type — every step
815
964
  and its step IO — into a scope and publish + verify it; dry-run by default),
965
+ `action_define` (define an existing action type's inputs, outputs and script/REST
966
+ steps with data-pill wiring, the way the Designer's Save does; dry-run by default,
967
+ idempotent, read back and verified, optional publish),
816
968
  `flow_publish` (compile a flow/subflow snapshot), `flow_copy`
817
969
  (copy a flow as an inactive draft), `flow_create` (create a NEW flow from scratch +
818
970
  publish, grafting a template), `flow_test` (validate or run a flow), and
package/dist/cli.js CHANGED
@@ -84,6 +84,7 @@ const createFlow_1 = require("./flowDesigner/createFlow");
84
84
  const editFlow_1 = require("./flowDesigner/editFlow");
85
85
  const editActionType_1 = require("./flowDesigner/editActionType");
86
86
  const cloneActionType_1 = require("./flowDesigner/cloneActionType");
87
+ const defineActionType_1 = require("./flowDesigner/defineActionType");
87
88
  const testFlow_1 = require("./flowDesigner/testFlow");
88
89
  const table_1 = require("./table");
89
90
  const setField_1 = require("./setField");
@@ -1094,6 +1095,145 @@ async function runCloneAction(flags, bare) {
1094
1095
  }
1095
1096
  return 0;
1096
1097
  }
1098
+ /**
1099
+ * dove-sn define-action:
1100
+ * --sys-id <sys_id> Required. The sys_hub_action_type_definition to define (the shell must
1101
+ * exist — make it with clone-action or the Flow Designer).
1102
+ * --scope <name|sys_id> Required. The action's own scope (x_cadso_email_spok or a 32-hex sys_id).
1103
+ * --spec <spec.json> Required. The definition — action inputs, outputs, steps (see below).
1104
+ * --update-set <sys_id> Optional. Pin the REST session to this update set before the save/publish.
1105
+ * --publish With --confirm: also publish (snapshot) after the save.
1106
+ * --confirm Execute (PUT the model, verify, optionally publish). WITHOUT it: dry-run.
1107
+ * --dry-run Force a dry-run even with --confirm.
1108
+ * --json Emit the structured DefineActionTypeResult.
1109
+ *
1110
+ * Saves the action the way the Flow Designer's Save does: GET the model + the
1111
+ * step graph, merge the spec, PUT the FULL model back to
1112
+ * /api/now/processflow/action/action_types/{id}, read it back to verify.
1113
+ * DRY-RUN BY DEFAULT: prints the planned diff, writes nothing. Idempotent: a
1114
+ * spec already in effect is "unchanged" and makes no PUT.
1115
+ *
1116
+ * --spec shape (every part optional — incremental edits are fine):
1117
+ * {
1118
+ * "action": { "name": "...", "description": "...", "access": "public" | "package_private" },
1119
+ * "inputs": [{ "name": "host", "type": "choice", "mandatory": true, "default": "api",
1120
+ * "choices": [{ "value": "api", "label": "API" }] }], // upsert by name; "remove": true
1121
+ * "outputs": [{ "name": "status_code", "value": "{{steps.call.status_code}}" }],
1122
+ * "steps": [
1123
+ * { "ref": "guard", "type": "script", "label": "Guard", "scriptFile": "guard.js",
1124
+ * "inputs": { "host_1": "{{action.host}}" }, "outputs": [{ "name": "base_url" }] },
1125
+ * { "ref": "call", "type": "rest", "label": "Call", "values": { "base_url": "{{steps.guard.base_url}}",
1126
+ * "http_method": "get", "headers": [{ "name": "Accept", "value": "application/json" }] } }
1127
+ * ]
1128
+ * }
1129
+ * `scriptFile` (resolved relative to the spec file) is sugar for `script`.
1130
+ */
1131
+ async function runDefineAction(flags, bare) {
1132
+ var bareErr = bareStringFlagError("define-action", bare, ["sys-id", "scope", "spec", "update-set"]);
1133
+ if (bareErr) {
1134
+ process.stderr.write(bareErr);
1135
+ return 1;
1136
+ }
1137
+ var sysId = flags["sys-id"] || flags.sysId;
1138
+ var scope = flags.scope;
1139
+ var specPath = flags.spec;
1140
+ if (!sysId || !scope || !specPath) {
1141
+ process.stderr.write("define-action: --sys-id <sys_id>, --scope <scope name|sys_id> and --spec <spec.json> are required\n");
1142
+ return 1;
1143
+ }
1144
+ var rawSpec = JSON.parse(fs.readFileSync(specPath, "utf8"));
1145
+ if (!rawSpec || typeof rawSpec !== "object" || Array.isArray(rawSpec)) {
1146
+ process.stderr.write("define-action: --spec must contain a JSON object\n");
1147
+ return 1;
1148
+ }
1149
+ resolveStepScriptFiles(rawSpec, specPath);
1150
+ var result = await (0, defineActionType_1.defineActionType)({
1151
+ client: (0, client_1.createClient)({}),
1152
+ sysId: sysId,
1153
+ scope: scope,
1154
+ spec: rawSpec,
1155
+ confirm: flags.confirm === "true",
1156
+ dryRun: flags["dry-run"] === "true",
1157
+ publish: flags.publish === "true",
1158
+ updateSetSysId: flags["update-set"],
1159
+ });
1160
+ if (flags.json === "true") {
1161
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1162
+ return result.verify && !result.verify.ok ? 1 : 0;
1163
+ }
1164
+ var d = result.diff;
1165
+ process.stdout.write("[" + result.status + "] " + result.after.action.name + " (" + result.sysId + ") in " +
1166
+ result.scope.name + "\n");
1167
+ d.action.forEach(function (c) {
1168
+ process.stdout.write(" action." + c.field + ": '" + c.before + "' -> '" + c.after + "'\n");
1169
+ });
1170
+ var named = function (kind, part) {
1171
+ part.added.forEach(function (n) {
1172
+ process.stdout.write(" + " + kind + " " + n + "\n");
1173
+ });
1174
+ part.changed.forEach(function (c) {
1175
+ process.stdout.write(" ~ " + kind + " " + c.name + ": " +
1176
+ c.changes.map(function (f) {
1177
+ return f.field + " '" + f.before + "' -> '" + f.after + "'";
1178
+ }).join(", ") + "\n");
1179
+ });
1180
+ part.removed.forEach(function (n) {
1181
+ process.stdout.write(" - " + kind + " " + n + "\n");
1182
+ });
1183
+ };
1184
+ named("input", d.inputs);
1185
+ named("output", d.outputs);
1186
+ var stepLine = function (sign, s) {
1187
+ process.stdout.write(" " + sign + " step " + s.order + " '" + s.label + "' [" + s.type + "] " + s.cid +
1188
+ (s.ref ? " (ref " + s.ref + (s.matchedBy ? ", matched by " + s.matchedBy : "") + ")" : "") + "\n");
1189
+ s.changes.forEach(function (c) {
1190
+ process.stdout.write(" " + c + "\n");
1191
+ });
1192
+ };
1193
+ d.steps.added.forEach(function (s) { stepLine("+", s); });
1194
+ d.steps.changed.forEach(function (s) { stepLine("~", s); });
1195
+ d.steps.removed.forEach(function (s) { stepLine("-", s); });
1196
+ if (d.empty) {
1197
+ process.stdout.write(" no changes — the spec is already in effect\n");
1198
+ }
1199
+ result.warnings.forEach(function (w) {
1200
+ process.stdout.write(" ! " + w + "\n");
1201
+ });
1202
+ if (result.status === "planned") {
1203
+ process.stdout.write("\nDRY RUN — nothing written. Re-run with --confirm (and --publish to snapshot) to save.\n");
1204
+ return 0;
1205
+ }
1206
+ if (result.verify) {
1207
+ process.stdout.write("\n--- verify (read back from the instance) ---\n " + (result.verify.ok ? "OK" : "FAILED") + "\n");
1208
+ result.verify.notes.forEach(function (n) {
1209
+ process.stdout.write(" " + (result.verify && result.verify.ok ? "+ " : "! ") + n + "\n");
1210
+ });
1211
+ }
1212
+ if (result.publish) {
1213
+ process.stdout.write("published (HTTP " + result.publish.httpStatus +
1214
+ (result.publish.snapshotSysId ? ", snapshot " + result.publish.snapshotSysId : "") + ")\n");
1215
+ }
1216
+ return result.verify && !result.verify.ok ? 1 : 0;
1217
+ }
1218
+ /** Replace each step's `scriptFile` with `script`, read relative to the spec file. */
1219
+ function resolveStepScriptFiles(spec, specPath) {
1220
+ var steps = spec.steps;
1221
+ if (!Array.isArray(steps)) {
1222
+ return;
1223
+ }
1224
+ var specDir = path.dirname(path.resolve(specPath));
1225
+ for (var i = 0; i < steps.length; i += 1) {
1226
+ var step = steps[i];
1227
+ if (!step || typeof step !== "object" || typeof step.scriptFile !== "string") {
1228
+ continue;
1229
+ }
1230
+ if (typeof step.script === "string") {
1231
+ throw new Error("define-action: step '" + String(step.ref) + "' sets both scriptFile and script — pick one.");
1232
+ }
1233
+ step.script = fs.readFileSync(path.resolve(specDir, step.scriptFile), "utf8");
1234
+ delete step.scriptFile;
1235
+ }
1236
+ }
1097
1237
  async function runMcp(flags) {
1098
1238
  if (flags.smoke === "true") {
1099
1239
  await (0, server_1.runSmoke)();
@@ -1215,6 +1355,13 @@ function printHelp() {
1215
1355
  " [--update-set <sys_id> (required with --confirm)] [--confirm] [--dry-run] [--json])\n" +
1216
1356
  " Idempotent on (name, scope). Publishes via the snapshot path and\n" +
1217
1357
  " reads the steps back to verify.\n" +
1358
+ " define-action Define a Custom Action Type's inputs, outputs and steps (script + REST,\n" +
1359
+ " data-pill wired) the way the Designer's Save does, then optionally publish\n" +
1360
+ " DRY-RUN BY DEFAULT — nothing is written without --confirm\n" +
1361
+ " (--sys-id <sys_id> --scope <scope name|sys_id> --spec <spec.json>\n" +
1362
+ " [--update-set <sys_id>] [--publish] [--confirm] [--dry-run] [--json])\n" +
1363
+ " Idempotent: a spec already in effect makes no write. The action shell\n" +
1364
+ " must exist (clone-action or the Designer).\n" +
1218
1365
  " edit-flow Patch a flow/subflow (rename, description, step inputs)\n" +
1219
1366
  " (--sys-id <sys_id> --from-json <ops.json> [--apply] [--update-set <sys_id>] [--scope <sys_id>] [--json])\n" +
1220
1367
  " publish-app Publish a scoped app to the ServiceNow Store, the company application\n" +
@@ -2424,6 +2571,9 @@ async function main() {
2424
2571
  if (parsed.command === "clone-action") {
2425
2572
  return await runCloneAction(parsed.flags, parsed.bare);
2426
2573
  }
2574
+ if (parsed.command === "define-action") {
2575
+ return await runDefineAction(parsed.flags, parsed.bare);
2576
+ }
2427
2577
  if (parsed.command === "create-view") {
2428
2578
  await runCreateView(parsed.flags);
2429
2579
  return 0;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Designer-shaped templates for `defineActionType` — GENERATED from two real
3
+ * Flow Designer saves of a Custom Action Type (the PUT to
4
+ * /api/now/processflow/action/action_types/{id}). Do not hand-edit the shapes:
5
+ * they are the exact key sets the Designer sends, with values reset to the
6
+ * step type's defaults. Record ids that belong to the STEP TYPE (each step
7
+ * input's `id` is its sys_hub_step_type input definition) are kept; ids that
8
+ * belong to one action (variable records, cids, uiUniqueIds) are blanked and
9
+ * minted fresh per use.
10
+ *
11
+ * Regenerate from new captures rather than editing by hand.
12
+ */
13
+ export interface StepTypeTemplate {
14
+ /** sys_hub_step_type sys_id. */
15
+ stepTypeId: string;
16
+ /** The PUT body's DB_TYPE for this step type. */
17
+ dbType: string;
18
+ /** Outputs every step of this type exposes (`{{step[cid].<name>}}`). */
19
+ builtinOutputs: Array<{
20
+ name: string;
21
+ label: string;
22
+ type: string;
23
+ }>;
24
+ /** One PUT-shaped step with values at their defaults. */
25
+ template: Record<string, unknown>;
26
+ }
27
+ export declare var STEP_TYPE_TEMPLATES: Record<"script" | "rest", StepTypeTemplate>;
28
+ /** A new action input (sys_hub_action_input) as the Designer first sends it. */
29
+ export declare var ACTION_INPUT_TEMPLATE: Record<string, unknown>;
30
+ /** A new action output (sys_hub_action_output) wired to a data pill. */
31
+ export declare var ACTION_OUTPUT_TEMPLATE: Record<string, unknown>;
32
+ /** A new script-step input variable (extended_inputs entry). */
33
+ export declare var STEP_EXT_INPUT_TEMPLATE: Record<string, unknown>;
34
+ /** A new script-step output variable (extended_outputs entry). */
35
+ export declare var STEP_EXT_OUTPUT_TEMPLATE: Record<string, unknown>;