@jacebenson/jsn 4.3.7-dev.17 → 4.3.7-dev.19

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
@@ -59,6 +59,7 @@ jsn flows executions --since "2026-08-25 00:00:00" --until "2026-08-26 00:00:00"
59
59
  jsn flows executions --summary # Server totals + sampled duration metrics by flow
60
60
  jsn flows executions --limit 100 # Inspect more rows from the matching population
61
61
  jsn flows executions --record <sys_id> # Executions for one source record
62
+ jsn flows create --data-file flow.manifest.json # Create a draft record-triggered flow
62
63
  jsn rules list --query "collection=incident"
63
64
  jsn updatesets set "My Feature"
64
65
 
@@ -93,8 +94,150 @@ jsn snippets run open-inc
93
94
  jsn logs follow --level error --tail 10
94
95
  ```
95
96
 
97
+ ### Flow Designer actions
98
+
99
+ `actions` and `action` are the same command. `list`, `show`, and explicit
100
+ `delete` remain available. Create and edit use full Process Flow JSON, not
101
+ ordinary table-field updates.
102
+
103
+ ```bash
104
+ jsn actions step-types --scope <scope_sys_id> --get data > step-types.json
105
+ jsn actions create --scope <scope_sys_id> --data-file new-action.json --json
106
+ jsn actions definition <action_sys_id> --get data > action.json
107
+ # Edit action.json, retaining its ID, scope, lifecycle fields and all arrays.
108
+ jsn actions update <action_sys_id> --data-file action.json --json
109
+ jsn action edit <action_sys_id> --data-file action.json --json # same operation
110
+ jsn actions test <action_sys_id> --force --output-map '{"response":"hello"}' --wait --timeout 60 --json
111
+ jsn actions test <action_sys_id> --force --data-file action.json --output-map '{"response":"hello"}' --wait --get data.outputs.response.value
112
+ jsn actions delete <action_sys_id> --force # explicit cleanup, never automatic
113
+ ```
114
+
115
+ Existing targets infer their exact transaction scope from the action record.
116
+ An explicit `--scope` must match that record byte-for-byte and exist in
117
+ `sys_scope`. For Global, use the literal sys_id `global`.
118
+
119
+ This **illustrative create document** shows an input/script/output mapping.
120
+ Replace `<SCRIPT_STEP_TYPE_SYS_ID>` with the target's `step-types` result.
121
+ Start from that step type's full input/output schemas, preserving runtime
122
+ settings, status outputs and any other required fields. The shortened example
123
+ is not a captured or instance-verified template. New steps/variables need their
124
+ own identifiers; do not clone another action's record IDs. Leave new steps'
125
+ `step_id` and `action` blank or omitted. Create binds their `action` to the newly
126
+ allocated parent; persisted step IDs or parent bindings are rejected before creation.
127
+
128
+ ```json
129
+ {
130
+ "name": "Echo response",
131
+ "description": "Return the supplied response",
132
+ "internal_name": "echo_response",
133
+ "inputs": [{"name": "response", "label": "Response", "type": "string", "mandatory": true}],
134
+ "outputs": [{"name": "response", "label": "Response", "type": "string", "value": "{{step[11111111-2222-4333-8444-555555555555].response}}"}],
135
+ "steps": [{
136
+ "cid": "11111111-2222-4333-8444-555555555555",
137
+ "step_type": "SCRIPT",
138
+ "step_type_id": "<SCRIPT_STEP_TYPE_SYS_ID>",
139
+ "label": "Echo script",
140
+ "order": 1,
141
+ "inputs": [
142
+ {"name": "required_run_time", "type": "choice", "value": "instance"},
143
+ {"name": "script", "type": "script", "value": "(function execute(inputs, outputs) { outputs.response = inputs.response; })(inputs, outputs);"}
144
+ ],
145
+ "outputs": [],
146
+ "extended_inputs": [{"name": "response", "type": "string", "value": "{{inputs.response}}", "extended": true}],
147
+ "extended_outputs": [{"name": "response", "type": "string", "extended": true}]
148
+ }]
149
+ }
150
+ ```
151
+
152
+ Create first inserts the parent in `sys_hub_action_type_definition`, reads that
153
+ parent's own Process Flow defaults, then saves the inputs, outputs and nonempty
154
+ steps with the internal `/api/now/processflow/action/action_types/{id}` PUT.
155
+ It retains fresh lifecycle/status metadata instead of copying source snapshot
156
+ or status IDs. A subsequent definition GET plus `/step_instances` GET verifies
157
+ scripts, mappings and schemas. Update requires a complete document from
158
+ `definition`, including target-owned lifecycle fields. Omitted fields are
159
+ rejected before mutation. A PUT timeout triggers readback, not another PUT;
160
+ `persisted_after_timeout` means persistence was verified, not that HTTP success
161
+ was received. Failed creation reports the parent ID and leaves cleanup to you.
162
+
163
+ Test sends `action`, `outputMap`, `runOnThread` and `tracingEnabled` to `/test`.
164
+ Despite its wire name, `--output-map` supplies **action inputs**. Without
165
+ `--wait`, success means dispatch only, with `status: dispatched` and no outputs.
166
+ With `--wait`, JSN requires a `COMPLETE` context, blank `error_message`, and each
167
+ declared user output's actual value from `sys_flow_runtime_value`. Output entries
168
+ have `value`, `displayValue` and `hasValue`; false, zero and empty strings count.
169
+ Failures, denied/missing outputs and timeouts exit nonzero. The wait budget
170
+ starts after dispatch, defaults to 60 seconds, and is capped at 3600 seconds.
171
+ Timeout does not cancel the server execution. Draft actions can be tested without
172
+ publication. Testing runs their scripts and can have side effects. It requires
173
+ interactive confirmation or `--force`, unless the profile explicitly skips
174
+ confirmations. Read-only profiles block create/update/delete/test. No publish
175
+ or snapshot command is added.
176
+ These internal endpoints and runtime-table permissions can vary by instance.
177
+
96
178
  ### Flow execution fields
97
179
 
180
+ ### Flow creation manifest
181
+
182
+ `jsn flows create` accepts `--data-file` or inline `--data`. The first slice
183
+ supports one `record_create_or_update` trigger and published Action types whose
184
+ input schemas are available from the instance. JSN resolves action metadata,
185
+ defaults, and typed inputs from ServiceNow; callers do not provide GraphQL or
186
+ editor-generated identifiers.
187
+
188
+ ```json
189
+ {
190
+ "name": "JSN-STUDY-ticket-log",
191
+ "description": "Log JSN study tickets",
192
+ "scope": "<SCOPE_SYS_ID>",
193
+ "trigger": {
194
+ "type": "record_create_or_update",
195
+ "table": "ticket",
196
+ "condition": "short_descriptionSTARTSWITHJSN-STUDY-"
197
+ },
198
+ "actions": [
199
+ { "type": "Log", "inputs": { "log_level": "info", "log_message": "Ticket received" } }
200
+ ]
201
+ }
202
+ ```
203
+
204
+ `scope` is required; JSN never defaults it to a tenant-specific or captured
205
+ scope. The record-trigger condition currently accepts one simple encoded-query
206
+ predicate (`fieldSTARTSWITHvalue`, `field=value`, `fieldLIKEvalue`, and the
207
+ equivalent single-predicate operators). Compound `^`/`^OR` expressions are
208
+ rejected until their predicate AST mapping is proven against the instance.
209
+
210
+ Creation leaves the flow in Draft and inactive, then creates and reads back an
211
+ Autosave version. `flows publish` remains a separate lifecycle operation: it
212
+ reads the saved ProcessFlow definition, POSTs `/snapshot`, creates an
213
+ `Activate/Publish` version, reads version history, and verifies `active=true`.
214
+ Do not use it in a dry-run: publishing activates the flow.
215
+
216
+ `jsn flows test <identifier> --record <sys_id>` is also a mutating operation.
217
+ It requires confirmation (or `--force`), reads the saved definition, verifies
218
+ that the supplied record exists in the record-trigger table, and POSTs the full
219
+ definition to the ProcessFlow test endpoint. A response with only a context ID
220
+ is reported as accepted, not completed. Test runs execute actions and can have
221
+ side effects; this slice does not execute test flows during development.
222
+
223
+ Flow Logic, subflows, flow-variable lifecycle, and error handling are deferred
224
+ and rejected by this slice rather than silently ignored.
225
+
226
+ `jsn flows update <identifier> --data-file update.json` updates supported fields
227
+ of existing inactive Draft flows. The manifest may contain only `trigger` (with
228
+ `table` and/or `condition`) and `actions`. Each action selects one saved instance
229
+ by its one-based `order` and reusable action `type` (name or sys_id). Its `inputs`
230
+ object may change only inputs already present in that saved Action. The CLI reads
231
+ the saved component identities itself; manifests never carry UI UUIDs. Omitted
232
+ trigger fields, Action inputs, metadata, and other flow components are preserved.
233
+ Names must resolve to exactly one flow, and the protected `jace-test-flow` is
234
+ rejected. Update sends the captured GraphQL `triggerInstances.update` and/or
235
+ `actions.update` shapes, reads the full ProcessFlow definition back, creates an
236
+ Autosave version, and reports update-set captures. It never publishes or
237
+ activates the flow. Unsupported Flow Logic, subflows, variables, error handling,
238
+ new Action inputs, ambiguous selectors, active flows, and non-Draft flows are
239
+ rejected before mutation.
240
+
98
241
  `jsn flows executions` reads `sys_flow_context` and returns both the raw row and a normalized `execution` object. JSN discovers the runtime columns from `sys_dictionary` first, then uses these mappings:
99
242
 
100
243
  - `started`: `started`, `start_time`, `started_at`, then `sys_created_on`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.7-dev.17",
3
+ "version": "4.3.7-dev.19",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,7 +45,7 @@ async function fetchExtensionChain(sdk, record) {
45
45
  return chain;
46
46
  }
47
47
 
48
- export const actionsCmd = (wrap) => buildDevCmd('actions', 'sys_hub_action_type_definition', ['action'], ['name', 'active', 'sys_scope', 'sys_updated_on'], wrap, { singular: 'action', scopeValidation: true });
48
+ export { actionsCmd } from './actions.js';
49
49
 
50
50
  export const includesCmd = (wrap) => buildDevCmd('includes', 'sys_script_include', ['include', 'si'], ['name', 'api_name', 'active', 'sys_scope'], wrap, { singular: 'script include', scopeValidation: true });
51
51
 
@@ -0,0 +1,122 @@
1
+ import { buildDevCmd } from './_generic.js';
2
+ import { parseDataArg, confirmDelete } from '../helpers.js';
3
+ import { resolveRecord, unwrapSysId } from '../resolve-record.js';
4
+ import { declareCapabilities } from '../capabilities.js';
5
+ import { ACTION_TABLE, actionScope, validateScope } from '../processflow-actions.js';
6
+ import { errUsage } from '../errors.js';
7
+
8
+ const scopeOption = y => y.option('scope', { type: 'string', describe: 'Exact transaction scope sys_id; inferred from existing action when omitted' });
9
+ const dataOptions = y => y
10
+ .option('data-file', { type: 'string', describe: 'Full action definition JSON file, not table fields' })
11
+ .option('data', { type: 'string', describe: 'Full action definition as JSON' });
12
+ async function actionID(argv, app) {
13
+ return unwrapSysId(await resolveRecord(app.sdk, { table: ACTION_TABLE, identifier: argv.identifier, matchField: 'name', resource: 'action' }));
14
+ }
15
+
16
+ export const actionsCmd = wrap => {
17
+ // Reuse the existing list/show picker and output contract only.
18
+ const base = buildDevCmd('actions', ACTION_TABLE, ['action'], ['name', 'active', 'sys_scope', 'sys_updated_on'], wrap, {
19
+ singular: 'action', readOnly: true,
20
+ showBreadcrumbs: (_record, id) => [
21
+ { action: 'definition', cmd: `jsn actions definition ${id}`, description: 'Read complete executable definition' },
22
+ { action: 'list', cmd: 'jsn actions list', description: 'Back to actions' },
23
+ ],
24
+ });
25
+ declareCapabilities('actions', { mutationSubcommands: ['create', 'update', 'edit', 'delete', 'test'] });
26
+ // Retain the common wrap and output controller; domain failures get a
27
+ // structured error envelope and nonzero exit, never a success-shaped dispatch.
28
+ const run = handler => wrap(async (argv, app) => {
29
+ try { await handler(argv, app); }
30
+ catch (error) {
31
+ if (!error.code?.startsWith('action_')) throw error;
32
+ if (error.details) error.hint = JSON.stringify(error.details);
33
+ app.err(error);
34
+ process.exitCode = 1;
35
+ }
36
+ });
37
+ return {
38
+ ...base,
39
+ describe: 'Flow Designer actions: create, edit full definitions, and test with outputs',
40
+ builder: y => base.builder(y)
41
+ .command({
42
+ command: 'create', describe: 'Create parent and executable Process Flow action; verify persistence',
43
+ builder: yy => dataOptions(yy).option('scope', { type: 'string', demandOption: true, describe: 'Exact target scope sys_id, or global' }),
44
+ handler: run(async (argv, app) => {
45
+ const definition = parseDataArg(argv);
46
+ const result = await app.sdk.createProcessFlowAction(argv.scope, definition);
47
+ app.ok(result, { summary: `Created and verified action ${result.sys_id}` });
48
+ }),
49
+ })
50
+ .command({
51
+ command: 'definition <identifier>', describe: 'Read full JSON definition including executable step_instances',
52
+ builder: scopeOption,
53
+ handler: run(async (argv, app) => {
54
+ const id = await actionID(argv, app);
55
+ const scope = await actionScope(app.sdk, id, argv.scope);
56
+ app.ok(await app.sdk.getProcessFlowAction(id, scope), { summary: `Read action definition ${id}` });
57
+ }),
58
+ })
59
+ .command({
60
+ command: 'update <identifier>', aliases: ['edit'], describe: 'Save a full definition through Process Flow PUT and verify readback',
61
+ builder: yy => dataOptions(scopeOption(yy)),
62
+ handler: run(async (argv, app) => {
63
+ const definition = parseDataArg(argv);
64
+ const id = await actionID(argv, app);
65
+ const result = await app.sdk.updateProcessFlowAction(id, argv.scope, definition);
66
+ app.ok(result, { summary: `Saved and verified action ${id}` });
67
+ }),
68
+ })
69
+ .command({
70
+ command: 'test <identifier>', describe: 'Execute saved or supplied definition; optionally wait for actual outputs',
71
+ builder: yy => dataOptions(scopeOption(yy))
72
+ .option('force', { type: 'boolean', default: false, describe: 'Confirm execution of action scripts, which may have side effects' })
73
+ .option('output-map', { type: 'string', default: '{}', describe: 'JSON action input names to values; sent as outputMap' })
74
+ .option('run-on-thread', { type: 'boolean', default: true, describe: 'Run test on a worker thread' })
75
+ .option('tracing-enabled', { type: 'boolean', default: false, describe: 'Enable Flow Designer tracing' })
76
+ .option('wait', { type: 'boolean', default: false, describe: 'Wait for COMPLETE context and actual runtime outputs' })
77
+ .option('timeout', { type: 'number', default: 60, describe: 'Maximum wait seconds after dispatch, greater than 0, at most 3600' }),
78
+ handler: run(async (argv, app) => {
79
+ let outputMap;
80
+ try { outputMap = JSON.parse(argv['output-map'] ?? '{}'); }
81
+ catch { throw errUsage('--output-map must be valid JSON'); }
82
+ const definition = argv['data-file'] || argv.data ? parseDataArg(argv) : undefined;
83
+ const id = await actionID(argv, app);
84
+ await confirmDelete(app, argv, `Execute action '${argv.identifier}' scripts, which may have side effects`);
85
+ const result = await app.sdk.testProcessFlowAction(id, argv.scope, definition, outputMap, {
86
+ wait: argv.wait, timeout: argv.timeout, runOnThread: argv['run-on-thread'], tracingEnabled: argv['tracing-enabled'],
87
+ });
88
+ app.ok(result, { summary: result.status === 'complete' ? `Test completed ${result.context}` : `Test dispatched ${result.context}; outputs not yet verified` });
89
+ }),
90
+ })
91
+ .command({
92
+ command: 'step-types', describe: 'Read step schema templates for a target scope',
93
+ builder: yy => yy.option('scope', { type: 'string', demandOption: true, describe: 'Exact transaction scope sys_id, or global' }),
94
+ handler: run(async (argv, app) => {
95
+ await validateScope(app.sdk, argv.scope);
96
+ app.ok(await app.sdk.getProcessFlowStepTypes(argv.scope), { summary: 'Process Flow step schemas' });
97
+ }),
98
+ })
99
+ .command({
100
+ command: 'delete <identifier>', describe: 'Delete action parent explicitly',
101
+ builder: yy => scopeOption(yy).option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
102
+ handler: run(async (argv, app) => {
103
+ const id = await actionID(argv, app);
104
+ await actionScope(app.sdk, id, argv.scope);
105
+ await confirmDelete(app, argv, `Delete action '${argv.identifier}'`);
106
+ await app.sdk.delete(ACTION_TABLE, id);
107
+ // Verify the exact target is absent. Do not interpret a 403 as deletion.
108
+ const query = new URLSearchParams({ sysparm_query: `sys_id=${id}`, sysparm_limit: '1', sysparm_fields: 'sys_id', sysparm_display_value: 'false' });
109
+ const response = await app.sdk.request(`${app.sdk.baseURL}/api/now/table/${ACTION_TABLE}?${query}`, { method: 'GET' });
110
+ if (!response || response.error || !Array.isArray(response.result) || response.result.length) {
111
+ throw new Error(`Delete is unverified; action ${id} absence was not explicitly confirmed`);
112
+ }
113
+ app.ok({ name: argv.identifier, sys_id: id, deleted: true }, { summary: `Deleted action ${id}` });
114
+ }),
115
+ }),
116
+ handler: wrap(async (_argv, app) => {
117
+ app.ok({ commands: ['list', 'show', 'definition', 'step-types', 'create', 'update', 'test', 'delete'] }, {
118
+ summary: 'Use jsn actions <command> --help. Create/update require full JSON definitions, not table fields.',
119
+ });
120
+ }),
121
+ };
122
+ };
@@ -1,9 +1,12 @@
1
- import { formatRecordForDisplay, getStringField, interactiveList } from '../helpers.js';
1
+ import fs from 'node:fs';
2
+ import { formatRecordForDisplay, getStringField, interactiveList, confirmDelete } from '../helpers.js';
2
3
  import { discoverFlowContextFields, normalizeFlowContext, summarizeFlowContexts, formatFlowContextSummary, aggregateFlowContextMappings, mergeFlowContextStats, buildFlowContextQuery } from '../flow-context.js';
3
4
  import { declareCapabilities } from '../capabilities.js';
4
5
  import { inspectFlow } from '../flow-inspection.js';
5
- import { publishFlows, publishDoctor, flowStatus } from '../flow-publish.js';
6
+ import { publishFlows, publishFlowLifecycle, publishDoctor, flowStatus } from '../flow-publish.js';
6
7
  import { resolveRecord, unwrapSysId } from '../resolve-record.js';
8
+ import { createFlowFromManifest, updateFlowFromManifest, readCurrentUpdateSet } from '../flow-create.js';
9
+ import { testFlow } from '../flow-test.js';
7
10
 
8
11
  function flowInspectionAdapter(app) {
9
12
  return {
@@ -12,7 +15,7 @@ function flowInspectionAdapter(app) {
12
15
  };
13
16
  }
14
17
 
15
- declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'delete', 'publish'] });
18
+ declareCapabilities('flows', { mutationSubcommands: ['create', 'update', 'delete', 'publish', 'test'] });
16
19
 
17
20
  function renderChecks(checks) {
18
21
  return checks.map((c) => {
@@ -179,9 +182,14 @@ export function flowsCmd(wrap) {
179
182
  resource: argv.action ? 'Custom action' : 'Flow',
180
183
  }));
181
184
 
182
- const result = argv.action
183
- ? await publishFlows(app.sdk, [], [sysID])
184
- : await publishFlows(app.sdk, [sysID], []);
185
+ if (!argv.action) {
186
+ const lifecycle = await publishFlowLifecycle(app.sdk, app.getEffectiveInstance(), sysID);
187
+ return app.ok(lifecycle, {
188
+ summary: `Published ${sysID}; active=${lifecycle.active}; version=${lifecycle.version?.sys_id ?? lifecycle.version?.id ?? 'read back'}`,
189
+ breadcrumbs: [{ action: 'status', cmd: `jsn flows status ${sysID}`, description: 'Verify the flow will run' }],
190
+ });
191
+ }
192
+ const result = await publishFlows(app.sdk, [], [sysID]);
185
193
 
186
194
  const { summary, results, via } = result;
187
195
  const lines = results.map(r => ` ${r.status === 'success' ? '✅' : '❌'} ${r.flow_name ?? r.sys_id}: ${r.message ?? r.status}${r.errorMessage ? ` — ${r.errorMessage}` : ''}`);
@@ -208,6 +216,23 @@ export function flowsCmd(wrap) {
208
216
  });
209
217
  }),
210
218
  })
219
+ .command({
220
+ command: 'test <identifier>',
221
+ describe: 'Execute a record-triggered flow against one record (may cause side effects)',
222
+ builder: (y) => y
223
+ .option('record', { type: 'string', demandOption: true, describe: 'Trigger record sys_id to test against' })
224
+ .option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
225
+ handler: wrap(async (argv, app) => {
226
+ app.requireInstance();
227
+ await confirmDelete(app, argv, `Execute flow test for ${argv.identifier} against record ${argv.record}; actions may cause side effects`);
228
+ const result = await testFlow(app.sdk, app.getEffectiveInstance(), argv.identifier, argv.record);
229
+ return app.ok(result, {
230
+ summary: result.status === 'completed'
231
+ ? `Flow test completed (${result.success ? 'success' : 'failed'})`
232
+ : 'Flow test accepted for execution',
233
+ });
234
+ }),
235
+ })
211
236
  .command({
212
237
  command: 'doctor',
213
238
  describe: 'Check whether this instance can publish flows at all',
@@ -224,22 +249,56 @@ export function flowsCmd(wrap) {
224
249
  })
225
250
  .command({
226
251
  command: 'create',
227
- describe: 'Create a new flow (not yet implemented)',
252
+ describe: 'Create a draft flow from a schema-driven JSON manifest',
228
253
  builder: (y) => y
229
- .option('data', { type: 'string', describe: 'JSON data for the flow' }),
230
- handler: wrap(async (_argv, _app) => {
231
- throw new Error('Flow creation requires the Flow Designer GraphQL API - not yet implemented.\n'
232
- + 'Use the ServiceNow web UI to create flows, then use "jsn flows list" to view them.');
254
+ .option('data', { type: 'string', describe: 'Flow manifest as inline JSON' })
255
+ .option('data-file', { type: 'string', describe: 'Read the flow manifest from a JSON file' }),
256
+ handler: wrap(async (argv, app) => {
257
+ app.requireInstance();
258
+ const dataFile = argv.dataFile ?? argv['data-file'];
259
+ if (!dataFile && argv.data === undefined) throw new Error('Provide --data-file or --data for the flow manifest');
260
+ let manifest;
261
+ try {
262
+ const raw = dataFile ? fs.readFileSync(dataFile, 'utf8') : argv.data;
263
+ manifest = JSON.parse(raw.charCodeAt?.(0) === 0xFEFF ? raw.slice(1) : raw);
264
+ } catch (error) {
265
+ throw new Error(`Invalid flow manifest JSON: ${error.message}`, { cause: error });
266
+ }
267
+ const result = await createFlowFromManifest(app.sdk, app.getEffectiveInstance(), manifest, {
268
+ readUpdateSet: () => readCurrentUpdateSet(app.sdk),
269
+ });
270
+ app.ok(result, {
271
+ summary: `Created draft flow "${result.flow.name}" (${result.flow.id}) without activation`,
272
+ breadcrumbs: [{ action: 'show', cmd: `jsn flows show ${result.flow.id}`, description: 'Inspect the draft flow' }],
273
+ });
233
274
  }),
234
275
  })
235
276
  .command({
236
277
  command: 'update <identifier>',
237
- describe: 'Update a flow (not yet implemented)',
278
+ describe: 'Safely update supported fields of an existing inactive Draft flow',
238
279
  builder: (y) => y
239
- .option('data', { type: 'string', describe: 'JSON data to update' }),
240
- handler: wrap(async (_argv, _app) => {
241
- throw new Error('Flow updates require the Flow Designer GraphQL API - not yet implemented.\n'
242
- + 'Use the ServiceNow web UI to update flows, then use "jsn flows list" to view them.');
280
+ .option('data', { type: 'string', describe: 'Update manifest as inline JSON' })
281
+ .option('data-file', { type: 'string', describe: 'Read the update manifest from a JSON file' })
282
+ .option('force', { type: 'boolean', default: false, describe: 'Skip confirmation' }),
283
+ handler: wrap(async (argv, app) => {
284
+ app.requireInstance();
285
+ const dataFile = argv.dataFile ?? argv['data-file'];
286
+ if (!dataFile && argv.data === undefined) throw new Error('Provide --data-file or --data for the flow update manifest');
287
+ let manifest;
288
+ try {
289
+ const raw = dataFile ? fs.readFileSync(dataFile, 'utf8') : argv.data;
290
+ manifest = JSON.parse(raw.charCodeAt?.(0) === 0xFEFF ? raw.slice(1) : raw);
291
+ } catch (error) {
292
+ throw new Error(`Invalid flow update manifest JSON: ${error.message}`, { cause: error });
293
+ }
294
+ await confirmDelete(app, argv, `Update draft flow ${argv.identifier}; unspecified components and inputs are preserved`);
295
+ const result = await updateFlowFromManifest(app.sdk, app.getEffectiveInstance(), argv.identifier, manifest, {
296
+ readUpdateSet: () => readCurrentUpdateSet(app.sdk),
297
+ });
298
+ return app.ok(result, {
299
+ summary: `Updated draft flow "${result.flow.name}" (${result.flow.id}) without activation`,
300
+ breadcrumbs: [{ action: 'show', cmd: `jsn flows show ${result.flow.id}`, description: 'Inspect the updated draft flow' }],
301
+ });
243
302
  }),
244
303
  })
245
304
  .command({
@@ -260,9 +319,10 @@ export function flowsCmd(wrap) {
260
319
  console.log(' show <identifier> Show flow details by name or sys_id');
261
320
  console.log(' status <identifier> Check whether a flow is actually able to run');
262
321
  console.log(' publish <identifier> Publish (activate) a flow');
322
+ console.log(' test <identifier> Execute a record-triggered flow against one record');
263
323
  console.log(' doctor Check whether this instance can publish flows');
264
- console.log(' create Create a new flow (not yet implemented)');
265
- console.log(' update <identifier> Update a flow (not yet implemented)');
324
+ console.log(' create Create a draft flow from a JSON manifest');
325
+ console.log(' update <identifier> Safely update supported fields of an existing Draft flow');
266
326
  console.log(' delete <identifier> Delete a flow (not yet implemented)');
267
327
  console.log('');
268
328
  console.log('Run "jsn flows <command> --help" for details.');