ultravisor 1.0.0 → 1.0.2

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.
Files changed (96) hide show
  1. package/.babelrc +6 -0
  2. package/.browserslistrc +1 -0
  3. package/.browserslistrc-BACKUP +1 -0
  4. package/.gulpfile-quackage-config.json +7 -0
  5. package/.gulpfile-quackage.js +2 -0
  6. package/CONTRIBUTING.md +50 -0
  7. package/README.md +34 -0
  8. package/debug/Harness.js +2 -1
  9. package/docs/.nojekyll +0 -0
  10. package/docs/_sidebar.md +18 -0
  11. package/docs/_topbar.md +7 -0
  12. package/docs/architecture.md +103 -0
  13. package/docs/cover.md +15 -0
  14. package/docs/features/api.md +230 -0
  15. package/docs/features/cli.md +182 -0
  16. package/docs/features/configuration.md +245 -0
  17. package/docs/features/manifests.md +177 -0
  18. package/docs/features/operations.md +292 -0
  19. package/docs/features/scheduling.md +179 -0
  20. package/docs/features/tasks.md +1857 -0
  21. package/docs/index.html +39 -0
  22. package/docs/overview.md +75 -0
  23. package/docs/quickstart.md +167 -0
  24. package/docs/retold-catalog.json +24 -0
  25. package/docs/retold-keyword-index.json +19 -0
  26. package/package.json +5 -2
  27. package/source/Ultravisor.cjs +2 -2
  28. package/source/cli/Ultravisor-CLIProgram.cjs +38 -0
  29. package/source/cli/commands/Ultravisor-Command-ScheduleOperation.cjs +26 -2
  30. package/source/cli/commands/Ultravisor-Command-ScheduleTask.cjs +26 -2
  31. package/source/cli/commands/Ultravisor-Command-ScheduleView.cjs +22 -0
  32. package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +49 -1
  33. package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +51 -1
  34. package/source/cli/commands/Ultravisor-Command-Stop.cjs +4 -0
  35. package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +91 -0
  36. package/source/config/Ultravisor-Default-Command-Configuration.cjs +6 -1
  37. package/source/services/Ultravisor-Hypervisor-Event-Base.cjs +18 -1
  38. package/source/services/Ultravisor-Hypervisor-State.cjs +213 -0
  39. package/source/services/Ultravisor-Hypervisor.cjs +225 -1
  40. package/source/services/Ultravisor-Operation-Manifest.cjs +150 -1
  41. package/source/services/Ultravisor-Operation.cjs +190 -1
  42. package/source/services/Ultravisor-Task.cjs +339 -1
  43. package/source/services/events/Ultravisor-Hypervisor-Event-Cron.cjs +71 -1
  44. package/source/services/tasks/Ultravisor-Task-Base.cjs +264 -0
  45. package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +188 -0
  46. package/source/services/tasks/Ultravisor-Task-Command.cjs +65 -0
  47. package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +190 -0
  48. package/source/services/tasks/Ultravisor-Task-Conditional.cjs +104 -0
  49. package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +72 -0
  50. package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +336 -0
  51. package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +143 -0
  52. package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +146 -0
  53. package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +158 -0
  54. package/source/services/tasks/Ultravisor-Task-Request.cjs +56 -0
  55. package/source/services/tasks/Ultravisor-Task-Solver.cjs +89 -0
  56. package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +93 -0
  57. package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +127 -0
  58. package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +119 -0
  59. package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +109 -0
  60. package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +112 -0
  61. package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +499 -0
  62. package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +150 -0
  63. package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +110 -0
  64. package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +89 -0
  65. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +87 -0
  66. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +67 -0
  67. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +66 -0
  68. package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +69 -0
  69. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +95 -0
  70. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +96 -0
  71. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +99 -0
  72. package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +102 -0
  73. package/source/web_server/Ultravisor-API-Server.cjs +463 -3
  74. package/test/Ultravisor_tests.js +6097 -1
  75. package/webinterface/.babelrc +6 -0
  76. package/webinterface/.browserslistrc +1 -0
  77. package/webinterface/.browserslistrc-BACKUP +1 -0
  78. package/webinterface/.gulpfile-quackage-config.json +7 -0
  79. package/webinterface/.gulpfile-quackage.js +2 -0
  80. package/webinterface/css/ultravisor.css +121 -0
  81. package/webinterface/html/index.html +32 -0
  82. package/webinterface/package.json +39 -0
  83. package/webinterface/source/Pict-Application-Ultravisor-Configuration.json +15 -0
  84. package/webinterface/source/Pict-Application-Ultravisor.js +414 -0
  85. package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +42 -0
  86. package/webinterface/source/views/PictView-Ultravisor-BottomBar.js +65 -0
  87. package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +236 -0
  88. package/webinterface/source/views/PictView-Ultravisor-Layout.js +83 -0
  89. package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +273 -0
  90. package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +243 -0
  91. package/webinterface/source/views/PictView-Ultravisor-OperationList.js +141 -0
  92. package/webinterface/source/views/PictView-Ultravisor-Schedule.js +280 -0
  93. package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +220 -0
  94. package/webinterface/source/views/PictView-Ultravisor-TaskList.js +248 -0
  95. package/webinterface/source/views/PictView-Ultravisor-TimingView.js +420 -0
  96. package/webinterface/source/views/PictView-Ultravisor-TopBar.js +147 -0
@@ -0,0 +1,1857 @@
1
+ # Tasks
2
+
3
+ Tasks are the fundamental unit of work in Ultravisor. Each task represents a
4
+ single executable action: a shell command, an HTTP request, or another
5
+ supported task type.
6
+
7
+ ## Task Model
8
+
9
+ Every task requires at minimum a `GUIDTask`. All other fields are optional
10
+ but recommended.
11
+
12
+ ```json
13
+ {
14
+ "GUIDTask": "my-task-001",
15
+ "Code": "MY_TASK",
16
+ "Name": "My First Task",
17
+ "Type": "Command",
18
+ "Command": "echo hello world",
19
+ "Parameters": "",
20
+ "Description": "A simple echo task for testing.",
21
+ "onBefore": [],
22
+ "onCompletion": [],
23
+ "onSubsequent": [],
24
+ "onFailure": [],
25
+ "onError": []
26
+ }
27
+ ```
28
+
29
+ ### Fields
30
+
31
+ | Field | Required | Description |
32
+ |-------|----------|-------------|
33
+ | `GUIDTask` | Yes | Unique identifier for the task |
34
+ | `Code` | No | Short code identifier |
35
+ | `Name` | No | Human-readable name |
36
+ | `Type` | No | Execution type (defaults to `Command`) |
37
+ | `Command` | No | Shell command to run (for Command type) |
38
+ | `Parameters` | No | Fallback for Command, or type-specific parameters |
39
+ | `Description` | No | Markdown description of the task |
40
+ | `URL` | No | Target URL (for Request type) |
41
+ | `Method` | No | HTTP method (for Request type, defaults to `GET`) |
42
+ | `onBefore` | No | Array of task GUIDs to execute before the core task |
43
+ | `onCompletion` | No | Array of task GUIDs to execute after a successful core task |
44
+ | `onFailure` | No | Array of task GUIDs to execute after a failed core task |
45
+ | `onError` | No | Array of task GUIDs to execute after a core task error |
46
+ | `onSubsequent` | No | Array of task GUIDs to always execute after the core task |
47
+ | `Destination` | No | Manyfest address in GlobalState for task output (see [Destination](#destination)) |
48
+ | `Persist` | No | Store task output to state or file (see [Persist](#persist)) |
49
+ | `Pattern` | No | Regular expression string (for LineMatch type) |
50
+ | `Flags` | No | Regex flags (for LineMatch type) |
51
+ | `Separator` | No | Split delimiter (for LineMatch type, defaults to newline) |
52
+
53
+ ## Task Types
54
+
55
+ ### Command
56
+
57
+ Executes a shell command via `child_process.exec`. The command string is
58
+ taken from the `Command` field, falling back to `Parameters` if `Command`
59
+ is not set.
60
+
61
+ ```json
62
+ {
63
+ "GUIDTask": "list-files",
64
+ "Name": "List Files in Home",
65
+ "Type": "Command",
66
+ "Command": "ls -la ~/"
67
+ }
68
+ ```
69
+
70
+ Default execution limits (configurable in `.ultravisor.json`):
71
+ - Timeout: 5 minutes (300,000 ms) — `UltravisorCommandTimeoutMilliseconds`
72
+ - Max output buffer: 10 MB — `UltravisorCommandMaxBufferBytes`
73
+
74
+ ### Request
75
+
76
+ Executes an HTTP request. The URL is taken from `URL`, falling back to
77
+ `Parameters`. Method defaults to `GET`.
78
+
79
+ | Field | Required | Description |
80
+ |-------|----------|-------------|
81
+ | `URL` | Yes | Endpoint to request (falls back to `Parameters`) |
82
+ | `Method` | No | HTTP method (defaults to `GET`) |
83
+ | `Persist` | No | Where to store the response (see [Persist](#persist)) |
84
+
85
+ ```json
86
+ {
87
+ "GUIDTask": "fetch-weather",
88
+ "Name": "Fetch Weather Data",
89
+ "Type": "Request",
90
+ "URL": "https://api.weather.example/current",
91
+ "Method": "GET"
92
+ }
93
+ ```
94
+
95
+ Request tasks use `curl` under the hood, so curl must be available on the
96
+ system.
97
+
98
+ ### ListFiles
99
+
100
+ Lists files and directories in the staging folder (or a sub-path within it).
101
+ Returns an array of file entries with name, size, type and modification time.
102
+
103
+ | Field | Required | Description |
104
+ |-------|----------|-------------|
105
+ | `Path` | No | Sub-directory within the staging folder to list |
106
+
107
+ ```json
108
+ {
109
+ "GUIDTask": "list-output",
110
+ "Name": "List Output Files",
111
+ "Type": "ListFiles",
112
+ "Path": "reports"
113
+ }
114
+ ```
115
+
116
+ Output is a JSON array of entries:
117
+
118
+ ```json
119
+ [
120
+ { "Name": "report-2026-02.csv", "Size": 14320, "IsDirectory": false, "Modified": "2026-02-10T08:30:00.000Z" },
121
+ { "Name": "archive", "Size": 4096, "IsDirectory": true, "Modified": "2026-02-09T12:00:00.000Z" }
122
+ ]
123
+ ```
124
+
125
+ ### WriteJSON
126
+
127
+ Serialises an object as pretty-printed JSON and writes it to a file in the
128
+ staging folder. Creates intermediate directories automatically.
129
+
130
+ | Field | Required | Description |
131
+ |-------|----------|-------------|
132
+ | `File` | Yes | Relative path inside the staging folder |
133
+ | `Data` | * | Object or value to serialise as JSON |
134
+ | `Address` | * | Dot-notation path into GlobalState to resolve the data to write |
135
+
136
+ \* Either `Data` or `Address` must be provided. When `Address` is set,
137
+ the data is resolved from `pContext.GlobalState` (useful when a previous
138
+ task stored its output in the shared state via `Destination`).
139
+
140
+ ```json
141
+ {
142
+ "GUIDTask": "save-config",
143
+ "Name": "Save Processed Config",
144
+ "Type": "WriteJSON",
145
+ "File": "output/processed-config.json",
146
+ "Data": {
147
+ "version": 3,
148
+ "features": ["scheduling", "manifests"],
149
+ "enabled": true
150
+ }
151
+ }
152
+ ```
153
+
154
+ Using `Address` to write data from a previous task's output:
155
+
156
+ ```json
157
+ {
158
+ "GUIDTask": "save-api-response",
159
+ "Name": "Save API Response",
160
+ "Type": "WriteJSON",
161
+ "File": "snapshots/api-response.json",
162
+ "Address": "APIData.Users"
163
+ }
164
+ ```
165
+
166
+ ### WriteText
167
+
168
+ Writes a plain text string to a file in the staging folder.
169
+ Creates intermediate directories automatically.
170
+
171
+ | Field | Required | Description |
172
+ |-------|----------|-------------|
173
+ | `File` | Yes | Relative path inside the staging folder |
174
+ | `Data` | * | String content to write |
175
+ | `Address` | * | Dot-notation path into GlobalState to resolve the text to write |
176
+
177
+ \* Either `Data` or `Address` must be provided. When `Address` is set,
178
+ the text is resolved from `pContext.GlobalState`.
179
+
180
+ ```json
181
+ {
182
+ "GUIDTask": "write-log",
183
+ "Name": "Write Status Log",
184
+ "Type": "WriteText",
185
+ "File": "logs/pipeline-status.log",
186
+ "Data": "Pipeline completed at 2026-02-10T12:00:00Z\nAll tasks succeeded."
187
+ }
188
+ ```
189
+
190
+ ### ReadJSON
191
+
192
+ Reads a JSON file from the staging folder, parses it and returns the
193
+ parsed object in `Output`.
194
+
195
+ | Field | Required | Description |
196
+ |-------|----------|-------------|
197
+ | `File` | Yes | Relative path inside the staging folder |
198
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
199
+
200
+ ```json
201
+ {
202
+ "GUIDTask": "load-config",
203
+ "Name": "Load Config",
204
+ "Type": "ReadJSON",
205
+ "File": "config/pipeline.json"
206
+ }
207
+ ```
208
+
209
+ The parsed JSON is available in `pManifestEntry.Output` as a serialised
210
+ JSON string.
211
+
212
+ ### ReadText
213
+
214
+ Reads a text file from the staging folder and returns its content in
215
+ `Output`.
216
+
217
+ | Field | Required | Description |
218
+ |-------|----------|-------------|
219
+ | `File` | Yes | Relative path inside the staging folder |
220
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
221
+
222
+ ```json
223
+ {
224
+ "GUIDTask": "read-template",
225
+ "Name": "Read Email Template",
226
+ "Type": "ReadText",
227
+ "File": "templates/notification.txt"
228
+ }
229
+ ```
230
+
231
+ ### WriteXML
232
+
233
+ Writes an XML string to a file in the staging folder.
234
+ Creates intermediate directories automatically. No XML validation is
235
+ performed — the caller is responsible for providing well-formed XML.
236
+
237
+ | Field | Required | Description |
238
+ |-------|----------|-------------|
239
+ | `File` | Yes | Relative path inside the staging folder |
240
+ | `Data` | * | XML string content to write |
241
+ | `Address` | * | Dot-notation path into GlobalState to resolve the XML to write |
242
+
243
+ \* Either `Data` or `Address` must be provided. When `Address` is set,
244
+ the XML content is resolved from `pContext.GlobalState`.
245
+
246
+ ```json
247
+ {
248
+ "GUIDTask": "write-config-xml",
249
+ "Name": "Write Config XML",
250
+ "Type": "WriteXML",
251
+ "File": "config/settings.xml",
252
+ "Data": "<?xml version=\"1.0\"?>\n<settings>\n <timeout>30</timeout>\n <retries>3</retries>\n</settings>"
253
+ }
254
+ ```
255
+
256
+ ### ReadXML
257
+
258
+ Reads an XML file from the staging folder and returns its content as a
259
+ raw string in `Output`. No XML parsing is performed — the caller is
260
+ responsible for interpreting the XML structure.
261
+
262
+ | Field | Required | Description |
263
+ |-------|----------|-------------|
264
+ | `File` | Yes | Relative path inside the staging folder |
265
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
266
+
267
+ ```json
268
+ {
269
+ "GUIDTask": "load-config-xml",
270
+ "Name": "Load Config XML",
271
+ "Type": "ReadXML",
272
+ "File": "config/settings.xml"
273
+ }
274
+ ```
275
+
276
+ ### ReadBinary
277
+
278
+ Reads a binary file from the staging folder and returns it as a Buffer.
279
+ The byte count is reported in `Output`.
280
+
281
+ | Field | Required | Description |
282
+ |-------|----------|-------------|
283
+ | `File` | Yes | Relative path inside the staging folder |
284
+ | `Destination` | No | Manyfest address in GlobalState, stored as base64 (defaults to `"Output"`) |
285
+ | `Persist` | No | Where to store the result (see [Persist](#persist)) |
286
+
287
+ ```json
288
+ {
289
+ "GUIDTask": "read-image",
290
+ "Name": "Read Image File",
291
+ "Type": "ReadBinary",
292
+ "File": "assets/logo.png"
293
+ }
294
+ ```
295
+
296
+ When persisting to a state address, the binary data is stored as a
297
+ base64-encoded string. When persisting to a file, the raw bytes are
298
+ written directly.
299
+
300
+ ### WriteBinary
301
+
302
+ Writes binary data to a file in the staging folder. Creates intermediate
303
+ directories automatically.
304
+
305
+ | Field | Required | Description |
306
+ |-------|----------|-------------|
307
+ | `File` | Yes | Relative path inside the staging folder |
308
+ | `Data` | Yes | Data to write -- Buffer, base64 string, or array of byte values |
309
+
310
+ ```json
311
+ {
312
+ "GUIDTask": "write-thumbnail",
313
+ "Name": "Write Thumbnail",
314
+ "Type": "WriteBinary",
315
+ "File": "thumbnails/frame-001.png",
316
+ "Data": "iVBORw0KGgo..."
317
+ }
318
+ ```
319
+
320
+ `Data` accepts three formats:
321
+
322
+ - **Buffer** -- written directly as binary
323
+ - **String** -- treated as base64-encoded and decoded before writing
324
+ - **Array** -- treated as an array of byte values (e.g. `[0xFF, 0xD8, 0xFF, 0xE0]`)
325
+
326
+ ### CopyFile
327
+
328
+ Copies a file from the local filesystem into the staging folder. This is
329
+ useful for importing external files (logs, configuration, data exports)
330
+ into an operation's staging area for further processing by subsequent tasks.
331
+ Creates intermediate directories in the destination path automatically.
332
+
333
+ | Field | Required | Description |
334
+ |-------|----------|-------------|
335
+ | `Source` | * | Absolute path to the local file to copy |
336
+ | `Address` | * | Dot-notation path into GlobalState containing the source path |
337
+ | `File` | Yes | Relative destination path inside the staging folder |
338
+
339
+ \* Either `Source` or `Address` must be provided. When `Address` is set,
340
+ the source path is resolved from `pContext.GlobalState` (or `NodeState`).
341
+
342
+ ```json
343
+ {
344
+ "GUIDTask": "import-config",
345
+ "Name": "Import External Config",
346
+ "Type": "CopyFile",
347
+ "Source": "/etc/myapp/config.json",
348
+ "File": "imported/config.json"
349
+ }
350
+ ```
351
+
352
+ Use `Address` to copy a file whose path was determined by a previous task:
353
+
354
+ ```json
355
+ {
356
+ "GUIDTask": "import-dynamic",
357
+ "Name": "Import Dynamic File",
358
+ "Type": "CopyFile",
359
+ "Address": "DiscoveredFilePath",
360
+ "File": "imports/data.csv"
361
+ }
362
+ ```
363
+
364
+ The source must be an existing regular file (not a directory). Path
365
+ traversal is blocked in the destination — file paths containing `..`
366
+ are rejected.
367
+
368
+ ### GetJSON
369
+
370
+ Performs a native HTTP/HTTPS GET request and parses the response body as
371
+ JSON. Uses Node.js built-in `http`/`https` modules (no curl dependency).
372
+
373
+ | Field | Required | Description |
374
+ |-------|----------|-------------|
375
+ | `URL` | Yes | Endpoint to request |
376
+ | `Headers` | No | Object of additional request headers |
377
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
378
+ | `Persist` | No | Where to store the parsed response (see [Persist](#persist)) |
379
+
380
+ ```json
381
+ {
382
+ "GUIDTask": "fetch-api-data",
383
+ "Name": "Fetch API Data",
384
+ "Type": "GetJSON",
385
+ "URL": "https://api.example.com/v1/status",
386
+ "Headers": {
387
+ "Authorization": "Bearer abc123"
388
+ }
389
+ }
390
+ ```
391
+
392
+ The parsed JSON response is available in `pManifestEntry.Output`. If the
393
+ response body is not valid JSON the task will error.
394
+
395
+ ### GetBinary
396
+
397
+ Performs a native HTTP/HTTPS GET request and collects the response as a
398
+ binary Buffer. Uses Node.js built-in `http`/`https` modules (no curl
399
+ dependency). The byte count is reported in `Output`.
400
+
401
+ | Field | Required | Description |
402
+ |-------|----------|-------------|
403
+ | `URL` | Yes | Endpoint to request |
404
+ | `Headers` | No | Object of additional request headers |
405
+ | `Destination` | No | Manyfest address in GlobalState, stored as base64 (defaults to `"Output"`) |
406
+ | `Persist` | No | Where to store the result (see [Persist](#persist)) |
407
+
408
+ ```json
409
+ {
410
+ "GUIDTask": "download-image",
411
+ "Name": "Download Product Image",
412
+ "Type": "GetBinary",
413
+ "URL": "https://cdn.example.com/images/product-001.png",
414
+ "Persist": { "File": "downloads/product-001.png" }
415
+ }
416
+ ```
417
+
418
+ When persisting to a state address, the binary data is stored as a
419
+ base64-encoded string. When persisting to a file, the raw bytes are
420
+ written directly.
421
+
422
+ ### GetText
423
+
424
+ Performs a native HTTP/HTTPS GET request and returns the response body as
425
+ plain text. Uses Node.js built-in `http`/`https` modules (no curl dependency).
426
+
427
+ | Field | Required | Description |
428
+ |-------|----------|-------------|
429
+ | `URL` | Yes | Endpoint to request |
430
+ | `Headers` | No | Object of additional request headers |
431
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
432
+ | `Persist` | No | Where to store the response text (see [Persist](#persist)) |
433
+
434
+ ```json
435
+ {
436
+ "GUIDTask": "fetch-readme",
437
+ "Name": "Fetch README",
438
+ "Type": "GetText",
439
+ "URL": "https://raw.githubusercontent.com/example/repo/main/README.md",
440
+ "Persist": "Files.ReadmeContent"
441
+ }
442
+ ```
443
+
444
+ The raw response text is available in `pManifestEntry.Output`. The
445
+ `Accept` header defaults to `text/plain` but can be overridden via the
446
+ `Headers` field.
447
+
448
+ ### GetXML
449
+
450
+ Performs a native HTTP/HTTPS GET request and returns the response body as
451
+ raw XML text. Uses Node.js built-in `http`/`https` modules (no curl
452
+ dependency). No XML parsing is performed — the caller is responsible
453
+ for interpreting the XML structure.
454
+
455
+ | Field | Required | Description |
456
+ |-------|----------|-------------|
457
+ | `URL` | Yes | Endpoint to request |
458
+ | `Headers` | No | Object of additional request headers |
459
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
460
+ | `Persist` | No | Where to store the response XML (see [Persist](#persist)) |
461
+
462
+ ```json
463
+ {
464
+ "GUIDTask": "fetch-feed",
465
+ "Name": "Fetch RSS Feed",
466
+ "Type": "GetXML",
467
+ "URL": "https://blog.example.com/feed.xml",
468
+ "Persist": { "File": "feeds/blog-feed.xml" }
469
+ }
470
+ ```
471
+
472
+ The raw XML string is available in `pManifestEntry.Output`. The `Accept`
473
+ header defaults to `application/xml, text/xml` but can be overridden via
474
+ the `Headers` field.
475
+
476
+ ### SendJSON
477
+
478
+ Sends JSON data to a REST URL using any HTTP method. Defaults to POST.
479
+ Uses Node.js built-in `http`/`https` modules (no curl dependency).
480
+
481
+ | Field | Required | Description |
482
+ |-------|----------|-------------|
483
+ | `URL` | Yes | Endpoint to request |
484
+ | `Method` | No | HTTP method (defaults to `POST`) |
485
+ | `Data` | No | Object to serialise and send as the request body |
486
+ | `Headers` | No | Object of additional request headers |
487
+ | `Persist` | No | Where to store the response (see [Persist](#persist)) |
488
+
489
+ ```json
490
+ {
491
+ "GUIDTask": "push-metrics",
492
+ "Name": "Push Metrics to Dashboard",
493
+ "Type": "SendJSON",
494
+ "URL": "https://dashboard.example.com/api/metrics",
495
+ "Method": "POST",
496
+ "Data": {
497
+ "source": "ultravisor",
498
+ "cpu": 42.5,
499
+ "memory": 1024
500
+ },
501
+ "Headers": {
502
+ "X-API-Key": "secret-key"
503
+ }
504
+ }
505
+ ```
506
+
507
+ PUT, PATCH and DELETE are also supported:
508
+
509
+ ```json
510
+ {
511
+ "GUIDTask": "update-record",
512
+ "Name": "Update Record",
513
+ "Type": "SendJSON",
514
+ "URL": "https://api.example.com/records/12345",
515
+ "Method": "PUT",
516
+ "Data": { "status": "archived" }
517
+ }
518
+ ```
519
+
520
+ ### RestRequest
521
+
522
+ A generic, fully configurable REST client task. Supports any HTTP method,
523
+ custom headers, request body, cookies and a shared cookie jar that
524
+ persists across tasks in an operation. This is the go-to task type when
525
+ the specialised Get/Send types are too restrictive.
526
+
527
+ | Field | Required | Description |
528
+ |-------|----------|-------------|
529
+ | `URL` | Yes | Endpoint to request |
530
+ | `Method` | No | HTTP method (defaults to `GET`) |
531
+ | `Body` | No | Request body -- object (serialised as JSON) or string |
532
+ | `ContentType` | No | `Content-Type` header value (auto-set to `application/json` when `Body` is an object) |
533
+ | `Headers` | No | Object of additional request headers |
534
+ | `Cookies` | No | Object of cookie name/value pairs to send |
535
+ | `StoreCookies` | No | Whether to capture `Set-Cookie` response headers (default: `true`) |
536
+ | `CaptureToken` | No | Extract a value from the JSON response body into the cookie jar (see [CaptureToken](#capturetoken)) |
537
+ | `CaptureHeader` | No | Extract response header values into GlobalState (see [CaptureHeader](#captureheader)) |
538
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
539
+ | `Persist` | No | Where to store the response (see [Persist](#persist)) |
540
+ | `Retries` | No | Number of retry attempts on failure (default: `0`; see [Retries](#retries)) |
541
+
542
+ The result stored at `Destination` (and in `pManifestEntry.Output`) is a
543
+ structured object:
544
+
545
+ ```json
546
+ {
547
+ "StatusCode": 200,
548
+ "Headers": { "content-type": "application/json", "..." : "..." },
549
+ "Body": "raw response text",
550
+ "JSON": { "parsed": "object if valid JSON" }
551
+ }
552
+ ```
553
+
554
+ The `JSON` field is only present when the response body is valid JSON.
555
+
556
+ #### Shared Cookie Jar
557
+
558
+ When a response includes `Set-Cookie` headers, the name/value pairs are
559
+ automatically parsed and stored at `pContext.GlobalState.Cookies`. Every
560
+ subsequent `RestRequest` task running in the same operation will
561
+ automatically include those cookies in its request — no extra
562
+ configuration needed.
563
+
564
+ Task-level `Cookies` merge on top of the shared jar, so explicit values
565
+ override jar values for that specific request without modifying the jar
566
+ itself.
567
+
568
+ Set `StoreCookies` to `false` to prevent a task from capturing response
569
+ cookies (the shared jar is still sent).
570
+
571
+ ```json
572
+ {
573
+ "GUIDTask": "api-login",
574
+ "Name": "API Login",
575
+ "Type": "RestRequest",
576
+ "URL": "https://api.example.com/auth/login",
577
+ "Method": "POST",
578
+ "Body": {
579
+ "username": "admin",
580
+ "password": "secret"
581
+ },
582
+ "Destination": "LoginResponse"
583
+ }
584
+ ```
585
+
586
+ After this task, `GlobalState.Cookies` will contain any session cookies
587
+ set by the server. A follow-up task automatically includes them:
588
+
589
+ ```json
590
+ {
591
+ "GUIDTask": "api-data",
592
+ "Name": "Fetch Protected Data",
593
+ "Type": "RestRequest",
594
+ "URL": "https://api.example.com/data",
595
+ "Destination": "ProtectedData"
596
+ }
597
+ ```
598
+
599
+ #### Sending non-JSON bodies
600
+
601
+ Use `Body` as a string with a custom `ContentType` to send XML, form
602
+ data, or any other format:
603
+
604
+ ```json
605
+ {
606
+ "GUIDTask": "post-xml",
607
+ "Name": "Post XML Payload",
608
+ "Type": "RestRequest",
609
+ "URL": "https://api.example.com/ingest",
610
+ "Method": "POST",
611
+ "Body": "<?xml version=\"1.0\"?><data><value>42</value></data>",
612
+ "ContentType": "application/xml"
613
+ }
614
+ ```
615
+
616
+ #### Overriding jar cookies
617
+
618
+ ```json
619
+ {
620
+ "GUIDTask": "impersonate",
621
+ "Name": "Impersonate User",
622
+ "Type": "RestRequest",
623
+ "URL": "https://api.example.com/profile",
624
+ "Cookies": { "session": "override-token" }
625
+ }
626
+ ```
627
+
628
+ The `session` cookie in the jar is overridden for this request only.
629
+
630
+ #### CaptureToken
631
+
632
+ Many APIs return session tokens in the JSON response body rather than
633
+ via `Set-Cookie` headers. `CaptureToken` extracts a value from the
634
+ parsed JSON response and stores it in `GlobalState.Cookies` so that
635
+ subsequent `RestRequest` tasks automatically send it as a cookie.
636
+
637
+ `CaptureToken` accepts two forms:
638
+
639
+ **String** -- a dot-notation path into the JSON response body. The
640
+ resolved value is stored as a cookie named `"Token"`:
641
+
642
+ ```json
643
+ {
644
+ "GUIDTask": "login",
645
+ "Type": "RestRequest",
646
+ "URL": "https://api.example.com/auth",
647
+ "Method": "POST",
648
+ "Body": { "user": "admin", "pass": "secret" },
649
+ "CaptureToken": "SessionToken"
650
+ }
651
+ ```
652
+
653
+ If the response body is `{"SessionToken": "abc123"}`, then
654
+ `GlobalState.Cookies.Token` is set to `"abc123"` and all subsequent
655
+ `RestRequest` tasks will send `Cookie: Token=abc123`.
656
+
657
+ **Object** -- with `Address` (dot-notation path) and `Cookie` (cookie
658
+ name to store the value under):
659
+
660
+ ```json
661
+ {
662
+ "GUIDTask": "login",
663
+ "Type": "RestRequest",
664
+ "URL": "https://api.example.com/auth",
665
+ "Method": "POST",
666
+ "Body": { "user": "admin", "pass": "secret" },
667
+ "CaptureToken": {
668
+ "Address": "Session.ID",
669
+ "Cookie": "SessionID"
670
+ }
671
+ }
672
+ ```
673
+
674
+ If the response body is `{"Session": {"ID": "xyz789"}}`, then
675
+ `GlobalState.Cookies.SessionID` is set to `"xyz789"`.
676
+
677
+ Nested paths are supported — the address walks the JSON structure
678
+ using dot-separated keys.
679
+
680
+ #### CaptureHeader
681
+
682
+ `CaptureHeader` extracts response header values and stores them at
683
+ manyfest addresses in `GlobalState`. This is useful for APIs that
684
+ return tokens, pagination cursors, or rate-limit information in
685
+ response headers.
686
+
687
+ `CaptureHeader` is an object mapping response header names to
688
+ GlobalState dot-notation addresses. Header names are matched
689
+ case-insensitively (Node.js lowercases all response headers).
690
+
691
+ ```json
692
+ {
693
+ "GUIDTask": "fetch-data",
694
+ "Type": "RestRequest",
695
+ "URL": "https://api.example.com/data",
696
+ "CaptureHeader": {
697
+ "X-Auth-Token": "AuthToken",
698
+ "X-Rate-Limit-Remaining": "RateLimits.Remaining",
699
+ "X-Total-Count": "Pagination.TotalCount"
700
+ }
701
+ }
702
+ ```
703
+
704
+ After this task executes, the captured header values are available at
705
+ their respective GlobalState addresses (e.g. `GlobalState.AuthToken`,
706
+ `GlobalState.RateLimits.Remaining`).
707
+
708
+ #### Retries
709
+
710
+ When `Retries` is set to a number greater than zero, a failed request
711
+ is automatically retried up to that many times before being marked as
712
+ an error. Retries apply to three failure modes:
713
+
714
+ - **Network errors** (connection refused, DNS resolution failure, etc.)
715
+ - **Timeouts** (request exceeds the configured timeout)
716
+ - **Non-2xx status codes** (HTTP 300+ responses)
717
+
718
+ Each retry waits 1 second before re-attempting. All retry attempts are
719
+ logged in the manifest entry's `Log` array so you can see exactly what
720
+ happened on each attempt.
721
+
722
+ ```json
723
+ {
724
+ "GUIDTask": "fetch-data",
725
+ "Name": "Fetch Data with Retries",
726
+ "Type": "RestRequest",
727
+ "URL": "https://api.example.com/data",
728
+ "Method": "GET",
729
+ "Retries": 3
730
+ }
731
+ ```
732
+
733
+ If all retries are exhausted without a successful response, the task is
734
+ marked with `Status: "Error"` and the log indicates how many attempts
735
+ were made. When `Retries` is `0` (the default), the existing behaviour
736
+ is unchanged -- a single failed request immediately marks the task as
737
+ an error.
738
+
739
+ ### GeneratePagedOperation
740
+
741
+ Generates and optionally auto-executes a paged operation from a template.
742
+ This task type enables two-phase data fetching: a planning phase that
743
+ determines the page count, followed by a fetching phase where each page
744
+ is a discrete, visible task. This design gives you progress bars (each
745
+ page task appears in the manifest) and per-page error visibility.
746
+
747
+ | Field | Required | Description |
748
+ |-------|----------|-------------|
749
+ | `RecordCount` | Yes | Total record count -- literal number or GlobalState address (string) |
750
+ | `MaximumRecordCount` | No | Cap the resolved `RecordCount` to this value (useful for fetching only the first N records) |
751
+ | `PageSize` | No | Records per page (default: `25`) |
752
+ | `TaskTemplate` | Yes | Template task definition. String values support `{PageStart}`, `{PageSize}`, `{PageIndex}`, `{PageCount}` interpolation |
753
+ | `OperationName` | No | Human-readable name for the generated operation |
754
+ | `AutoExecute` | No | Execute the generated operation immediately (default: `true`) |
755
+ | `Retries` | No | Number of retries per generated page task (default: `0`) |
756
+ | `Destination` | No | GlobalState address to store the generated operation GUID |
757
+
758
+ #### Template Interpolation
759
+
760
+ All string values in `TaskTemplate` are scanned for these variables:
761
+
762
+ | Variable | Description | Example values |
763
+ |----------|-------------|----------------|
764
+ | `{PageStart}` | Record offset (zero-based) | `0`, `25`, `50`, ... |
765
+ | `{PageSize}` | Records per page | `25` |
766
+ | `{PageIndex}` | Zero-based page number | `0`, `1`, `2`, ... |
767
+ | `{PageCount}` | Total number of pages | `4` |
768
+
769
+ Interpolation is recursive -- variables in nested objects, arrays and
770
+ URL strings are all replaced.
771
+
772
+ Each generated task automatically receives:
773
+ - `Destination: "Pages[{PageIndex}]"` -- results stored in an array
774
+ - `Retries` from the parent task definition (if set)
775
+ - `Name: "Page {N} of {Total}"` for clear manifest entries
776
+
777
+ #### How It Works
778
+
779
+ 1. Resolves `RecordCount` from GlobalState or literal value
780
+ 2. Calculates page count: `Math.ceil(RecordCount / PageSize)`
781
+ 3. Clones `TaskTemplate` for each page, replacing interpolation variables
782
+ 4. Writes a standalone config file to the staging folder for inspection
783
+ 5. Registers tasks and operation in memory (no config file pollution)
784
+ 6. If `AutoExecute` is true, executes the child operation with the
785
+ current `GlobalState` (cookies and auth tokens flow through)
786
+ 7. Cleans up ephemeral tasks from memory after execution
787
+
788
+ #### Example: Paged API Fetch
789
+
790
+ ```json
791
+ {
792
+ "Tasks": {
793
+ "authenticate": {
794
+ "GUIDTask": "authenticate",
795
+ "Type": "RestRequest",
796
+ "URL": "https://api.example.com/1.0/Authenticate",
797
+ "Method": "POST",
798
+ "Body": { "UserName": "user@example.com", "Password": "secret" },
799
+ "Destination": "AuthResponse"
800
+ },
801
+ "get-count": {
802
+ "GUIDTask": "get-count",
803
+ "Type": "RestRequest",
804
+ "URL": "https://api.example.com/1.0/DataFilter/Count",
805
+ "Method": "POST",
806
+ "Body": { "IDProject": 8605 },
807
+ "Destination": "CountResponse"
808
+ },
809
+ "extract-count": {
810
+ "GUIDTask": "extract-count",
811
+ "Type": "Solver",
812
+ "Expression": "TotalCount = {CountResponse.JSON.Count}"
813
+ },
814
+ "generate-and-fetch": {
815
+ "GUIDTask": "generate-and-fetch",
816
+ "Type": "GeneratePagedOperation",
817
+ "RecordCount": "TotalCount",
818
+ "PageSize": 25,
819
+ "TaskTemplate": {
820
+ "Type": "RestRequest",
821
+ "URL": "https://api.example.com/1.0/DataFilter/{PageStart}/{PageSize}",
822
+ "Method": "POST",
823
+ "Body": { "IDProject": 8605 }
824
+ },
825
+ "OperationName": "Fetch All Data",
826
+ "AutoExecute": true,
827
+ "Retries": 2
828
+ }
829
+ },
830
+ "Operations": {
831
+ "fetch-all-data": {
832
+ "GUIDOperation": "fetch-all-data",
833
+ "Tasks": ["authenticate", "get-count", "extract-count", "generate-and-fetch"]
834
+ }
835
+ }
836
+ }
837
+ ```
838
+
839
+ **Execution flow:**
840
+
841
+ 1. **authenticate** -- logs in, cookies captured in the shared jar
842
+ 2. **get-count** -- asks the API how many records match the filter
843
+ 3. **extract-count** -- Solver pulls the count into `GlobalState.TotalCount`
844
+ 4. **generate-and-fetch** -- calculates pages, generates N page-fetch
845
+ tasks, writes a standalone config to staging, then auto-executes the
846
+ child operation. Cookies from step 1 flow through to every page fetch.
847
+
848
+ The generated child operation has N tasks visible in its manifest,
849
+ enabling progress tracking. If page 7 times out after 2 retries, the
850
+ manifest shows exactly which page failed, the retry attempts and error
851
+ messages. Remaining pages still execute.
852
+
853
+ #### Output
854
+
855
+ When `AutoExecute` is `false`, the task output is the generated
856
+ operation GUID (a string). You can inspect the standalone config file
857
+ at `{StagingPath}/PagedOperation_{GUID}.json`.
858
+
859
+ When `AutoExecute` is `true`, the task output is a JSON object:
860
+
861
+ ```json
862
+ {
863
+ "OperationGUID": "generate-and-fetch-paged-1770836906553",
864
+ "PageCount": 12,
865
+ "ChildManifestSummary": "Operation ... Complete: 12 task(s) executed.",
866
+ "ChildManifestStatus": "Complete",
867
+ "ChildManifestSuccess": true
868
+ }
869
+ ```
870
+
871
+ #### Zero Records
872
+
873
+ When `RecordCount` is `0`, the task completes successfully with no
874
+ pages generated and a log message indicating there was nothing to fetch.
875
+
876
+ ### Solver
877
+
878
+ Evaluates a mathematical or logical expression using the fable
879
+ ExpressionParser. The operation's `GlobalState` is passed as the
880
+ Record (data source object), so expressions can reference any value
881
+ stored in the shared state using `{VariableName}` syntax.
882
+
883
+ | Field | Required | Description |
884
+ |-------|----------|-------------|
885
+ | `Expression` | Yes | Expression string to evaluate |
886
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
887
+
888
+ ```json
889
+ {
890
+ "GUIDTask": "calc-area",
891
+ "Name": "Calculate Area",
892
+ "Type": "Solver",
893
+ "Expression": "Area = {Width} * {Height}",
894
+ "Destination": "Calculations.Area"
895
+ }
896
+ ```
897
+
898
+ If the expression contains an assignment (e.g. `Area = {Width} * {Height}`),
899
+ the assigned variable is merged back into `GlobalState` so subsequent
900
+ tasks can reference it. The raw result is also stored at the `Destination`
901
+ address.
902
+
903
+ `GlobalState` is exposed at `AppData.GlobalState` so that expressions
904
+ using `getvalue("AppData.GlobalState.SomePath")` can also access it.
905
+
906
+ The solver supports the full fable ExpressionParser feature set:
907
+ arithmetic, comparison, logical operators, 100+ built-in functions
908
+ (SUM, MEAN, ROUND, SQRT, etc.), and directives like SERIES and MAP.
909
+
910
+ ```json
911
+ {
912
+ "GUIDTask": "round-total",
913
+ "Name": "Round Total",
914
+ "Type": "Solver",
915
+ "Expression": "ROUND({RawTotal}, 2)",
916
+ "Destination": "FinalTotal"
917
+ }
918
+ ```
919
+
920
+ ### Conditional
921
+
922
+ Evaluates an address from the execution context and branches to one of
923
+ two tasks based on whether the value is truthy or falsy.
924
+
925
+ | Field | Required | Description |
926
+ |-------|----------|-------------|
927
+ | `Address` | * | Dot-notation path into GlobalState or NodeState |
928
+ | `Value` | * | Literal value to test (alternative to Address) |
929
+ | `TrueTask` | No | GUID of the task to execute when truthy |
930
+ | `FalseTask` | No | GUID of the task to execute when falsy |
931
+
932
+ \* Either `Address` or `Value` must be provided.
933
+
934
+ The `Address` field resolves against `pContext.GlobalState` first, then
935
+ falls back to `pContext.NodeState`. Use dot-notation for nested paths
936
+ (e.g. `Config.Database.Enabled`).
937
+
938
+ ```json
939
+ {
940
+ "Tasks": {
941
+ "check-flag": {
942
+ "GUIDTask": "check-flag",
943
+ "Name": "Check Feature Flag",
944
+ "Type": "Conditional",
945
+ "Address": "Flags.UseNewPipeline",
946
+ "TrueTask": "run-new-pipeline",
947
+ "FalseTask": "run-legacy-pipeline"
948
+ },
949
+ "run-new-pipeline": {
950
+ "GUIDTask": "run-new-pipeline",
951
+ "Name": "New Pipeline",
952
+ "Type": "Command",
953
+ "Command": "bash /scripts/new_pipeline.sh"
954
+ },
955
+ "run-legacy-pipeline": {
956
+ "GUIDTask": "run-legacy-pipeline",
957
+ "Name": "Legacy Pipeline",
958
+ "Type": "Command",
959
+ "Command": "bash /scripts/legacy_pipeline.sh"
960
+ }
961
+ }
962
+ }
963
+ ```
964
+
965
+ When executed as part of an operation with GlobalState:
966
+
967
+ ```json
968
+ {
969
+ "GUIDOperation": "pipeline-op",
970
+ "Name": "Pipeline",
971
+ "Tasks": ["check-flag"],
972
+ "GlobalState": {
973
+ "Flags": { "UseNewPipeline": true }
974
+ }
975
+ }
976
+ ```
977
+
978
+ If neither `TrueTask` nor `FalseTask` matches the branch, the task
979
+ completes as a no-op.
980
+
981
+ Output includes the branch taken and the result of the selected task:
982
+
983
+ ```json
984
+ {
985
+ "Branch": "true",
986
+ "Task": "run-new-pipeline",
987
+ "Result": { "GUIDTask": "run-new-pipeline", "Status": "Complete", "Success": true, "..." : "..." }
988
+ }
989
+ ```
990
+
991
+ ### LineMatch
992
+
993
+ Splits a string on a separator (default: newline) and applies a regular
994
+ expression to each line, producing a JSON array of match result objects.
995
+ This is useful for parsing structured text output from commands, log files,
996
+ or any multi-line string into a structured array that subsequent tasks can
997
+ process.
998
+
999
+ | Field | Required | Description |
1000
+ |-------|----------|-------------|
1001
+ | `Address` | * | Dot-notation path into GlobalState for the input string |
1002
+ | `Data` | * | Inline string to process (used when Address is not set) |
1003
+ | `Pattern` | Yes | Regular expression string to apply to each line |
1004
+ | `Flags` | No | Regex flags (e.g. `"i"` for case-insensitive, default: `""`) |
1005
+ | `Separator` | No | String to split on (default: `"\n"`) |
1006
+ | `Destination` | No | Manyfest address in GlobalState (defaults to `"Output"`) |
1007
+
1008
+ \* Either `Address` or `Data` must be provided.
1009
+
1010
+ ```json
1011
+ {
1012
+ "GUIDTask": "parse-csv-lines",
1013
+ "Name": "Parse CSV Lines",
1014
+ "Type": "LineMatch",
1015
+ "Data": "Alice,30,Engineering\nBob,25,Marketing\nCharlie,35,Sales",
1016
+ "Pattern": "(\\w+),(\\d+),(\\w+)",
1017
+ "Destination": "ParsedRecords"
1018
+ }
1019
+ ```
1020
+
1021
+ Each element in the output array is an object with the following fields:
1022
+
1023
+ | Field | Type | Description |
1024
+ |-------|------|-------------|
1025
+ | `Index` | number | Zero-based line number |
1026
+ | `Line` | string | The full original line text |
1027
+ | `Match` | boolean | Whether the pattern matched this line |
1028
+ | `FullMatch` | string\|null | The entire matched substring, or null if no match |
1029
+ | `Groups` | array | Array of captured group values (numbered groups) |
1030
+ | `NamedGroups` | object | Object of named capture groups (only present if pattern uses `(?<name>...)`) |
1031
+
1032
+ Example output:
1033
+
1034
+ ```json
1035
+ [
1036
+ {
1037
+ "Index": 0,
1038
+ "Line": "Alice,30,Engineering",
1039
+ "Match": true,
1040
+ "FullMatch": "Alice,30,Engineering",
1041
+ "Groups": ["Alice", "30", "Engineering"]
1042
+ },
1043
+ {
1044
+ "Index": 1,
1045
+ "Line": "Bob,25,Marketing",
1046
+ "Match": true,
1047
+ "FullMatch": "Bob,25,Marketing",
1048
+ "Groups": ["Bob", "25", "Marketing"]
1049
+ }
1050
+ ]
1051
+ ```
1052
+
1053
+ Lines that do not match the pattern are still included in the array with
1054
+ `Match: false`, `FullMatch: null`, and an empty `Groups` array. This
1055
+ preserves line indices and allows subsequent tasks to identify which lines
1056
+ did not conform to the expected pattern.
1057
+
1058
+ Use with `Address` to process output from a previous task stored in
1059
+ GlobalState:
1060
+
1061
+ ```json
1062
+ {
1063
+ "GUIDTask": "parse-log",
1064
+ "Name": "Parse Log Output",
1065
+ "Type": "LineMatch",
1066
+ "Address": "CommandOutput",
1067
+ "Pattern": "^(\\d{4}-\\d{2}-\\d{2})\\s+(\\w+):\\s+(.*)",
1068
+ "Destination": "ParsedLog"
1069
+ }
1070
+ ```
1071
+
1072
+ Use a custom `Separator` to split on characters other than newline:
1073
+
1074
+ ```json
1075
+ {
1076
+ "GUIDTask": "parse-delimited",
1077
+ "Name": "Parse Pipe-Delimited",
1078
+ "Type": "LineMatch",
1079
+ "Data": "red|green|blue",
1080
+ "Pattern": "(\\w+)",
1081
+ "Separator": "|"
1082
+ }
1083
+ ```
1084
+
1085
+ ### Destination
1086
+
1087
+ The `Destination` parameter is available on all Read, Get, Solver,
1088
+ LineMatch and RestRequest task types (`ReadJSON`, `ReadText`, `ReadXML`,
1089
+ `ReadBinary`, `GetJSON`, `GetText`, `GetXML`, `GetBinary`, `Solver`,
1090
+ `LineMatch`, `RestRequest`).
1091
+ It declares where the task output is stored in
1092
+ `pContext.GlobalState` using a manyfest dot-notation address.
1093
+
1094
+ If `Destination` is not set, the default address is `"Output"`, which
1095
+ means the data is stored at `pContext.GlobalState.Output`. This allows
1096
+ subsequent tasks in the same operation to access the result directly
1097
+ from the shared state.
1098
+
1099
+ ```json
1100
+ {
1101
+ "GUIDTask": "fetch-users",
1102
+ "Type": "GetJSON",
1103
+ "URL": "https://api.example.com/users",
1104
+ "Destination": "APIData.Users"
1105
+ }
1106
+ ```
1107
+
1108
+ After execution, `pContext.GlobalState.APIData.Users` contains the
1109
+ parsed JSON response. A `Conditional` task or any subsequent task can
1110
+ then reference `APIData.Users` to branch on or process the data.
1111
+
1112
+ For binary task types (`ReadBinary`, `GetBinary`), the data is stored
1113
+ as a base64-encoded string at the destination address.
1114
+
1115
+ ### Persist
1116
+
1117
+ The `Persist` parameter is available on all RESTful and binary-reading
1118
+ task types (`Request`, `GetJSON`, `GetText`, `GetXML`, `GetBinary`,
1119
+ `SendJSON`, `RestRequest`, `ReadBinary`).
1120
+ It controls where the task output is stored after execution.
1121
+
1122
+ `Persist` accepts three forms:
1123
+
1124
+ **String** -- a manyfest dot-notation address. The result is stored into
1125
+ `pContext.GlobalState` at the given path:
1126
+
1127
+ ```json
1128
+ {
1129
+ "GUIDTask": "fetch-status",
1130
+ "Type": "GetJSON",
1131
+ "URL": "https://api.example.com/status",
1132
+ "Persist": "APIResults.Status"
1133
+ }
1134
+ ```
1135
+
1136
+ After execution, `pContext.GlobalState.APIResults.Status` contains the
1137
+ parsed JSON response. Subsequent tasks in the same operation can read
1138
+ this value via `Conditional` or any other context-aware mechanism.
1139
+
1140
+ **Object with `Address`** -- identical to the string form but wrapped in
1141
+ an object:
1142
+
1143
+ ```json
1144
+ {
1145
+ "Persist": { "Address": "APIResults.Status" }
1146
+ }
1147
+ ```
1148
+
1149
+ **Object with `File`** -- writes the result to a file relative to the
1150
+ staging folder:
1151
+
1152
+ ```json
1153
+ {
1154
+ "Persist": { "File": "snapshots/api-status.json" }
1155
+ }
1156
+ ```
1157
+
1158
+ For binary data (`ReadBinary`, `GetBinary`), persisting to an address
1159
+ stores the data as a base64-encoded string. Persisting to a file writes
1160
+ the raw bytes.
1161
+
1162
+ ### Staging Folder
1163
+
1164
+ All file-based task types (`ListFiles`, `WriteJSON`, `WriteText`,
1165
+ `WriteXML`, `WriteBinary`, `CopyFile`, `ReadJSON`, `ReadText`, `ReadXML`,
1166
+ `ReadBinary`) operate relative to the **staging folder**.
1167
+
1168
+ When tasks run inside an operation, each operation automatically gets its
1169
+ own staging folder at `{UltravisorStagingRoot}/{GUIDOperation}/`. This
1170
+ keeps each operation's files isolated. The staging folder is resolved in
1171
+ this order:
1172
+
1173
+ 1. `pContext.StagingPath` (set automatically per-operation, or overridden
1174
+ via `StagingPath` on the operation definition)
1175
+ 2. `UltravisorFileStorePath` from configuration
1176
+ 3. `${cwd}/dist/ultravisor_datastore` (fallback)
1177
+
1178
+ When a task runs standalone (not inside an operation), it falls back to
1179
+ options 2 and 3 above.
1180
+
1181
+ The operation also writes a `Manifest_{GUIDOperation}.json` file into
1182
+ the staging folder when the operation completes. This provides a
1183
+ persistent on-disk record of the operation's results alongside any files
1184
+ the tasks produced.
1185
+
1186
+ Path traversal is blocked -- file paths containing `..` are rejected.
1187
+
1188
+ ### Future Types
1189
+
1190
+ The following types are defined in the README but not yet implemented.
1191
+ Tasks with these types will return a manifest entry with
1192
+ `Status: "Unsupported"`:
1193
+
1194
+ - **Browser** -- headless browser navigation and interaction
1195
+ - **Browser Read** -- headless browser data reading
1196
+ - **Browser Action** -- click, navigate, fill actions
1197
+ - **Database Table** -- create tables in the output data store
1198
+ - **Integration** -- Meadow integration tasks
1199
+
1200
+ ## Task Execution Result
1201
+
1202
+ Every task execution produces a manifest entry:
1203
+
1204
+ ```json
1205
+ {
1206
+ "GUIDTask": "list-files",
1207
+ "Name": "List Files in Home",
1208
+ "Type": "Command",
1209
+ "StartTime": "2026-02-10T12:00:00.000Z",
1210
+ "StopTime": "2026-02-10T12:00:00.045Z",
1211
+ "Status": "Complete",
1212
+ "Success": true,
1213
+ "Output": "total 48\ndrwxr-x--- 12 user ...",
1214
+ "Log": [
1215
+ "Task list-files started at 2026-02-10T12:00:00.000Z",
1216
+ "Executing command: ls -la ~/",
1217
+ "stdout: total 48...",
1218
+ "Command completed successfully."
1219
+ ],
1220
+ "SubsequentResults": {}
1221
+ }
1222
+ ```
1223
+
1224
+ ### Status Values
1225
+
1226
+ | Status | Meaning |
1227
+ |--------|---------|
1228
+ | `Running` | Task is currently executing |
1229
+ | `Complete` | Task finished successfully |
1230
+ | `Error` | Task encountered an error or non-zero exit |
1231
+ | `Unsupported` | Task type is not yet implemented |
1232
+
1233
+ ## Subsequent Tasks
1234
+
1235
+ Subsequent tasks allow you to chain additional tasks around a core task's
1236
+ execution. Each subsequent task set is an array of task GUIDs that execute
1237
+ in sequence. Five built-in sets provide hooks into different points of the
1238
+ task lifecycle:
1239
+
1240
+ | Set | When It Runs | Condition |
1241
+ |-----|-------------|-----------|
1242
+ | `onBefore` | Before the core task | Always |
1243
+ | `onCompletion` | After the core task | Only if core task succeeded |
1244
+ | `onFailure` | After the core task | Only if core task failed |
1245
+ | `onError` | After the core task | Only if core task errored |
1246
+ | `onSubsequent` | After all conditional sets | Always (success or failure) |
1247
+
1248
+ All five sets are optional. Any set can contain zero or more task GUIDs.
1249
+ Tasks within a set execute sequentially in array order.
1250
+
1251
+ ### Execution Order
1252
+
1253
+ ```
1254
+ 1. onBefore[0], onBefore[1], ... (always runs)
1255
+ 2. Core task execution
1256
+ 3. If success → onCompletion[0], onCompletion[1], ...
1257
+ If failure → onFailure[0], onFailure[1], ...
1258
+ If error → onError[0], onError[1], ...
1259
+ 4. onSubsequent[0], onSubsequent[1], ... (always runs)
1260
+ ```
1261
+
1262
+ ### Basic Example
1263
+
1264
+ A task that runs a notification before and cleanup after:
1265
+
1266
+ ```json
1267
+ {
1268
+ "Tasks": {
1269
+ "notify-start": {
1270
+ "GUIDTask": "notify-start",
1271
+ "Name": "Notify Start",
1272
+ "Type": "Command",
1273
+ "Command": "echo 'Backup starting...' >> /var/log/ultravisor.log"
1274
+ },
1275
+ "backup-db": {
1276
+ "GUIDTask": "backup-db",
1277
+ "Name": "Backup Database",
1278
+ "Type": "Command",
1279
+ "Command": "pg_dump mydb > /backups/mydb.sql",
1280
+ "onBefore": ["notify-start"],
1281
+ "onCompletion": ["verify-backup", "notify-success"],
1282
+ "onFailure": ["notify-failure"],
1283
+ "onSubsequent": ["cleanup-temp"]
1284
+ },
1285
+ "verify-backup": {
1286
+ "GUIDTask": "verify-backup",
1287
+ "Name": "Verify Backup",
1288
+ "Type": "Command",
1289
+ "Command": "test -s /backups/mydb.sql && echo 'OK'"
1290
+ },
1291
+ "notify-success": {
1292
+ "GUIDTask": "notify-success",
1293
+ "Name": "Notify Success",
1294
+ "Type": "Command",
1295
+ "Command": "echo 'Backup completed successfully' >> /var/log/ultravisor.log"
1296
+ },
1297
+ "notify-failure": {
1298
+ "GUIDTask": "notify-failure",
1299
+ "Name": "Notify Failure",
1300
+ "Type": "Command",
1301
+ "Command": "echo 'Backup FAILED' >> /var/log/ultravisor.log"
1302
+ },
1303
+ "cleanup-temp": {
1304
+ "GUIDTask": "cleanup-temp",
1305
+ "Name": "Cleanup Temp Files",
1306
+ "Type": "Command",
1307
+ "Command": "rm -f /tmp/ultravisor-backup-*"
1308
+ }
1309
+ }
1310
+ }
1311
+ ```
1312
+
1313
+ When `backup-db` executes:
1314
+ 1. `notify-start` runs first (onBefore)
1315
+ 2. `pg_dump mydb > /backups/mydb.sql` runs (core task)
1316
+ 3. If the backup succeeds: `verify-backup` then `notify-success` run (onCompletion)
1317
+ 4. If the backup fails: `notify-failure` runs (onFailure)
1318
+ 5. `cleanup-temp` always runs last (onSubsequent)
1319
+
1320
+ ### Error Handling Example
1321
+
1322
+ Use `onError` for tasks that should run when the core task encounters
1323
+ an execution error (non-zero exit code, timeout, etc.):
1324
+
1325
+ ```json
1326
+ {
1327
+ "Tasks": {
1328
+ "deploy-app": {
1329
+ "GUIDTask": "deploy-app",
1330
+ "Name": "Deploy Application",
1331
+ "Type": "Command",
1332
+ "Command": "kubectl apply -f /deploy/manifest.yaml",
1333
+ "onBefore": ["run-tests", "build-image"],
1334
+ "onCompletion": ["smoke-test"],
1335
+ "onError": ["rollback-deploy", "alert-oncall"],
1336
+ "onSubsequent": ["log-deploy-result"]
1337
+ },
1338
+ "run-tests": {
1339
+ "GUIDTask": "run-tests",
1340
+ "Name": "Run Test Suite",
1341
+ "Type": "Command",
1342
+ "Command": "npm test"
1343
+ },
1344
+ "build-image": {
1345
+ "GUIDTask": "build-image",
1346
+ "Name": "Build Docker Image",
1347
+ "Type": "Command",
1348
+ "Command": "docker build -t myapp:latest ."
1349
+ },
1350
+ "smoke-test": {
1351
+ "GUIDTask": "smoke-test",
1352
+ "Name": "Run Smoke Tests",
1353
+ "Type": "Request",
1354
+ "URL": "https://myapp.example.com/health",
1355
+ "Method": "GET"
1356
+ },
1357
+ "rollback-deploy": {
1358
+ "GUIDTask": "rollback-deploy",
1359
+ "Name": "Rollback Deployment",
1360
+ "Type": "Command",
1361
+ "Command": "kubectl rollout undo deployment/myapp"
1362
+ },
1363
+ "alert-oncall": {
1364
+ "GUIDTask": "alert-oncall",
1365
+ "Name": "Alert On-Call",
1366
+ "Type": "Request",
1367
+ "URL": "https://hooks.slack.example.com/alert",
1368
+ "Method": "POST"
1369
+ },
1370
+ "log-deploy-result": {
1371
+ "GUIDTask": "log-deploy-result",
1372
+ "Name": "Log Deploy Result",
1373
+ "Type": "Command",
1374
+ "Command": "echo \"Deploy finished at $(date)\" >> /var/log/deploys.log"
1375
+ }
1376
+ }
1377
+ }
1378
+ ```
1379
+
1380
+ ### Manifest Entry with Subsequent Results
1381
+
1382
+ When a task with subsequent sets executes, the manifest entry includes a
1383
+ `SubsequentResults` object keyed by set name. Each set contains an array
1384
+ of manifest entries for the subsequent tasks that ran:
1385
+
1386
+ ```json
1387
+ {
1388
+ "GUIDTask": "backup-db",
1389
+ "Name": "Backup Database",
1390
+ "Type": "Command",
1391
+ "StartTime": "2026-02-10T12:00:00.000Z",
1392
+ "StopTime": "2026-02-10T12:00:02.150Z",
1393
+ "Status": "Complete",
1394
+ "Success": true,
1395
+ "Output": "pg_dump: ...",
1396
+ "Log": ["..."],
1397
+ "SubsequentResults": {
1398
+ "onBefore": [
1399
+ {
1400
+ "GUIDTask": "notify-start",
1401
+ "Status": "Complete",
1402
+ "Success": true,
1403
+ "Output": "..."
1404
+ }
1405
+ ],
1406
+ "onCompletion": [
1407
+ {
1408
+ "GUIDTask": "verify-backup",
1409
+ "Status": "Complete",
1410
+ "Success": true,
1411
+ "Output": "OK"
1412
+ },
1413
+ {
1414
+ "GUIDTask": "notify-success",
1415
+ "Status": "Complete",
1416
+ "Success": true,
1417
+ "Output": "..."
1418
+ }
1419
+ ],
1420
+ "onSubsequent": [
1421
+ {
1422
+ "GUIDTask": "cleanup-temp",
1423
+ "Status": "Complete",
1424
+ "Success": true,
1425
+ "Output": ""
1426
+ }
1427
+ ]
1428
+ }
1429
+ }
1430
+ ```
1431
+
1432
+ Sets that were not executed (e.g., `onFailure` when the task succeeded)
1433
+ will not appear in `SubsequentResults`.
1434
+
1435
+ ### Important Notes
1436
+
1437
+ - **No recursive chaining.** Subsequent tasks execute their core logic
1438
+ only. If a subsequent task itself has subsequent sets defined, those
1439
+ nested sets are not executed. This prevents infinite recursion.
1440
+ - **Missing GUIDs are skipped.** If a subsequent set references a task
1441
+ GUID that does not exist in state, it is logged and skipped gracefully.
1442
+ - **Empty arrays are no-ops.** Setting a subsequent set to `[]` is the
1443
+ same as not defining it at all.
1444
+ - **Order is preserved.** Tasks within a set execute sequentially in the
1445
+ order they appear in the array.
1446
+
1447
+ ## Managing Tasks
1448
+
1449
+ ### Via CLI
1450
+
1451
+ ```bash
1452
+ # Add or update a task
1453
+ ultravisor updatetask -g my-task -n "My Task" -t Command -p "echo hello"
1454
+
1455
+ # Add from a JSON file
1456
+ ultravisor updatetask -f ./task-definition.json
1457
+
1458
+ # Combine file + overrides (CLI params take precedence)
1459
+ ultravisor updatetask -f ./task-definition.json -g override-guid
1460
+
1461
+ # Run immediately
1462
+ ultravisor singletask my-task
1463
+
1464
+ # Dry run
1465
+ ultravisor singletask my-task --dry_run
1466
+ ```
1467
+
1468
+ ### Via API
1469
+
1470
+ ```bash
1471
+ # Create
1472
+ curl -X POST http://localhost:54321/Task \
1473
+ -H "Content-Type: application/json" \
1474
+ -d '{
1475
+ "GUIDTask": "my-task",
1476
+ "Name": "My Task",
1477
+ "Type": "Command",
1478
+ "Command": "echo hello"
1479
+ }'
1480
+
1481
+ # Read one
1482
+ curl http://localhost:54321/Task/my-task
1483
+
1484
+ # List all
1485
+ curl http://localhost:54321/Task
1486
+
1487
+ # Update
1488
+ curl -X PUT http://localhost:54321/Task/my-task \
1489
+ -H "Content-Type: application/json" \
1490
+ -d '{"Name": "My Updated Task", "Command": "echo updated"}'
1491
+
1492
+ # Delete
1493
+ curl -X DELETE http://localhost:54321/Task/my-task
1494
+
1495
+ # Execute
1496
+ curl http://localhost:54321/Task/my-task/Execute
1497
+ ```
1498
+
1499
+ ### Via Configuration File
1500
+
1501
+ Tasks defined directly in `.ultravisor.json` are loaded at startup:
1502
+
1503
+ ```json
1504
+ {
1505
+ "Tasks": {
1506
+ "backup-db": {
1507
+ "GUIDTask": "backup-db",
1508
+ "Name": "Backup Database",
1509
+ "Type": "Command",
1510
+ "Command": "pg_dump mydb > /backups/mydb.sql"
1511
+ },
1512
+ "fetch-api-data": {
1513
+ "GUIDTask": "fetch-api-data",
1514
+ "Name": "Fetch API Data",
1515
+ "Type": "Request",
1516
+ "URL": "https://api.example.com/data",
1517
+ "Method": "GET"
1518
+ }
1519
+ }
1520
+ }
1521
+ ```
1522
+
1523
+ ## Examples
1524
+
1525
+ ### Fetch public JSON and save locally
1526
+
1527
+ This operation pulls user data from the JSONPlaceholder API, saves the
1528
+ raw response to the staging folder, then lists the folder contents to
1529
+ confirm the file landed.
1530
+
1531
+ ```json
1532
+ {
1533
+ "UltravisorFileStorePath": "/var/data/ultravisor",
1534
+ "Tasks": {
1535
+ "fetch-users": {
1536
+ "GUIDTask": "fetch-users",
1537
+ "Name": "Fetch Users from JSONPlaceholder",
1538
+ "Type": "GetJSON",
1539
+ "URL": "https://jsonplaceholder.typicode.com/users"
1540
+ },
1541
+ "save-users": {
1542
+ "GUIDTask": "save-users",
1543
+ "Name": "Save Users to Staging",
1544
+ "Type": "WriteJSON",
1545
+ "File": "api-snapshots/users.json",
1546
+ "Data": "<<populated at runtime by the operation>>"
1547
+ },
1548
+ "verify-snapshot": {
1549
+ "GUIDTask": "verify-snapshot",
1550
+ "Name": "List Snapshot Directory",
1551
+ "Type": "ListFiles",
1552
+ "Path": "api-snapshots"
1553
+ }
1554
+ },
1555
+ "Operations": {
1556
+ "snapshot-users": {
1557
+ "GUIDOperation": "snapshot-users",
1558
+ "Name": "Snapshot Users API",
1559
+ "Tasks": ["fetch-users", "save-users", "verify-snapshot"]
1560
+ }
1561
+ }
1562
+ }
1563
+ ```
1564
+
1565
+ Running `ultravisor singleoperation snapshot-users` will:
1566
+ 1. GET `https://jsonplaceholder.typicode.com/users` and parse the JSON
1567
+ 2. Write the result to `/var/data/ultravisor/api-snapshots/users.json`
1568
+ 3. List the `api-snapshots/` directory and confirm the file exists
1569
+
1570
+ Each task's output is captured in the operation manifest, so the raw
1571
+ JSON from step 1 is available in the manifest even if step 2 fails.
1572
+
1573
+ ### Config-driven conditional pipeline
1574
+
1575
+ This configuration uses a `Conditional` task to check a feature flag
1576
+ before deciding which data pipeline to run. A `WriteJSON` task seeds
1577
+ a local config file that other tasks can read.
1578
+
1579
+ ```json
1580
+ {
1581
+ "UltravisorFileStorePath": "/data/pipeline",
1582
+ "Tasks": {
1583
+ "write-pipeline-config": {
1584
+ "GUIDTask": "write-pipeline-config",
1585
+ "Name": "Write Pipeline Config",
1586
+ "Type": "WriteJSON",
1587
+ "File": "config/pipeline.json",
1588
+ "Data": {
1589
+ "version": 2,
1590
+ "useNewParser": true,
1591
+ "outputFormat": "parquet"
1592
+ }
1593
+ },
1594
+ "read-pipeline-config": {
1595
+ "GUIDTask": "read-pipeline-config",
1596
+ "Name": "Read Pipeline Config",
1597
+ "Type": "ReadJSON",
1598
+ "File": "config/pipeline.json"
1599
+ },
1600
+ "check-parser-flag": {
1601
+ "GUIDTask": "check-parser-flag",
1602
+ "Name": "Check Parser Flag",
1603
+ "Type": "Conditional",
1604
+ "Address": "Flags.useNewParser",
1605
+ "TrueTask": "run-new-parser",
1606
+ "FalseTask": "run-legacy-parser"
1607
+ },
1608
+ "run-new-parser": {
1609
+ "GUIDTask": "run-new-parser",
1610
+ "Name": "Run New Parser",
1611
+ "Type": "Command",
1612
+ "Command": "python3 /scripts/new_parser.py --format parquet"
1613
+ },
1614
+ "run-legacy-parser": {
1615
+ "GUIDTask": "run-legacy-parser",
1616
+ "Name": "Run Legacy Parser",
1617
+ "Type": "Command",
1618
+ "Command": "python3 /scripts/legacy_parser.py --format csv"
1619
+ },
1620
+ "log-result": {
1621
+ "GUIDTask": "log-result",
1622
+ "Name": "Log Pipeline Result",
1623
+ "Type": "WriteText",
1624
+ "File": "logs/pipeline-run.log",
1625
+ "Data": "Pipeline completed successfully."
1626
+ }
1627
+ },
1628
+ "Operations": {
1629
+ "data-pipeline": {
1630
+ "GUIDOperation": "data-pipeline",
1631
+ "Name": "Conditional Data Pipeline",
1632
+ "Tasks": [
1633
+ "write-pipeline-config",
1634
+ "read-pipeline-config",
1635
+ "check-parser-flag",
1636
+ "log-result"
1637
+ ],
1638
+ "GlobalState": {
1639
+ "Flags": { "useNewParser": true }
1640
+ }
1641
+ }
1642
+ }
1643
+ }
1644
+ ```
1645
+
1646
+ ### Webhook relay with error notification
1647
+
1648
+ This example uses `SendJSON` to push metrics to a dashboard, with
1649
+ `onError` and `onCompletion` subsequent tasks for alerting. If the
1650
+ POST fails, an error report is written to the staging folder.
1651
+
1652
+ ```json
1653
+ {
1654
+ "UltravisorFileStorePath": "/var/data/ultravisor",
1655
+ "Tasks": {
1656
+ "push-metrics": {
1657
+ "GUIDTask": "push-metrics",
1658
+ "Name": "Push Metrics to Dashboard",
1659
+ "Type": "SendJSON",
1660
+ "URL": "https://dashboard.example.com/api/v1/ingest",
1661
+ "Method": "POST",
1662
+ "Data": {
1663
+ "source": "ultravisor",
1664
+ "timestamp": "2026-02-10T12:00:00Z",
1665
+ "cpu_percent": 34.2,
1666
+ "memory_mb": 2048,
1667
+ "disk_free_gb": 120
1668
+ },
1669
+ "Headers": {
1670
+ "X-API-Key": "your-dashboard-api-key"
1671
+ },
1672
+ "onCompletion": ["log-push-success"],
1673
+ "onError": ["write-error-report", "alert-slack"]
1674
+ },
1675
+ "log-push-success": {
1676
+ "GUIDTask": "log-push-success",
1677
+ "Name": "Log Success",
1678
+ "Type": "WriteText",
1679
+ "File": "logs/metrics-push.log",
1680
+ "Data": "Metrics pushed successfully."
1681
+ },
1682
+ "write-error-report": {
1683
+ "GUIDTask": "write-error-report",
1684
+ "Name": "Write Error Report",
1685
+ "Type": "WriteJSON",
1686
+ "File": "errors/last-push-failure.json",
1687
+ "Data": {
1688
+ "event": "metrics-push-failed",
1689
+ "action": "investigate dashboard endpoint"
1690
+ }
1691
+ },
1692
+ "alert-slack": {
1693
+ "GUIDTask": "alert-slack",
1694
+ "Name": "Alert Slack Channel",
1695
+ "Type": "SendJSON",
1696
+ "URL": "https://hooks.slack.com/services/T00/B00/xxxxx",
1697
+ "Method": "POST",
1698
+ "Data": {
1699
+ "text": "Ultravisor: metrics push to dashboard failed."
1700
+ }
1701
+ }
1702
+ }
1703
+ }
1704
+ ```
1705
+
1706
+ ### Multi-format report generator
1707
+
1708
+ Combines `GetJSON`, `WriteJSON`, `WriteText` and `ListFiles` into a
1709
+ reporting pipeline. Fetches data from a public API, stores the raw
1710
+ JSON, generates a human-readable summary, then lists all output files.
1711
+
1712
+ ```json
1713
+ {
1714
+ "UltravisorFileStorePath": "/data/reports",
1715
+ "Tasks": {
1716
+ "fetch-posts": {
1717
+ "GUIDTask": "fetch-posts",
1718
+ "Name": "Fetch Recent Posts",
1719
+ "Type": "GetJSON",
1720
+ "URL": "https://jsonplaceholder.typicode.com/posts?_limit=5"
1721
+ },
1722
+ "save-raw-json": {
1723
+ "GUIDTask": "save-raw-json",
1724
+ "Name": "Save Raw JSON",
1725
+ "Type": "WriteJSON",
1726
+ "File": "daily/posts-raw.json",
1727
+ "Data": { "note": "Replaced at runtime with fetch output" }
1728
+ },
1729
+ "write-summary": {
1730
+ "GUIDTask": "write-summary",
1731
+ "Name": "Write Human Summary",
1732
+ "Type": "WriteText",
1733
+ "File": "daily/summary.txt",
1734
+ "Data": "Daily Report\n============\nGenerated by Ultravisor.\n\nFetched 5 recent posts from JSONPlaceholder API.\nRaw data saved to posts-raw.json."
1735
+ },
1736
+ "list-output": {
1737
+ "GUIDTask": "list-output",
1738
+ "Name": "List Daily Output",
1739
+ "Type": "ListFiles",
1740
+ "Path": "daily"
1741
+ }
1742
+ },
1743
+ "Operations": {
1744
+ "daily-report": {
1745
+ "GUIDOperation": "daily-report",
1746
+ "Name": "Daily Report Pipeline",
1747
+ "Tasks": [
1748
+ "fetch-posts",
1749
+ "save-raw-json",
1750
+ "write-summary",
1751
+ "list-output"
1752
+ ]
1753
+ }
1754
+ }
1755
+ }
1756
+ ```
1757
+
1758
+ Running `ultravisor singleoperation daily-report` produces:
1759
+ - `daily/posts-raw.json` -- the raw API response
1760
+ - `daily/summary.txt` -- a text summary
1761
+ - The final manifest includes a file listing of the `daily/` directory
1762
+
1763
+ ### Authenticated REST session with shared cookies
1764
+
1765
+ This example uses `RestRequest` to log in to an API and then fetch
1766
+ protected data using the session established during authentication.
1767
+ Two mechanisms ensure the auth token flows to subsequent requests:
1768
+
1769
+ - **Set-Cookie headers** are automatically captured into
1770
+ `GlobalState.Cookies` (for APIs that use HTTP cookies).
1771
+ - **CaptureToken** extracts a token from the JSON response body
1772
+ and stores it in the cookie jar (for APIs that return tokens in
1773
+ the response body rather than via `Set-Cookie`).
1774
+
1775
+ The `save-observations` task uses the `Address` field to write
1776
+ data from `GlobalState` rather than a static `Data` value.
1777
+
1778
+ A working version of this example lives in
1779
+ `example_operations/headlight-observations/.ultravisor.json`.
1780
+
1781
+ ```json
1782
+ {
1783
+ "Tasks": {
1784
+ "authenticate": {
1785
+ "GUIDTask": "authenticate",
1786
+ "Name": "Authenticate to API",
1787
+ "Type": "RestRequest",
1788
+ "URL": "https://api.example.com/1.0/Authenticate",
1789
+ "Method": "POST",
1790
+ "Body": {
1791
+ "UserName": "user@example.com",
1792
+ "Password": "secret"
1793
+ },
1794
+ "CaptureToken": {
1795
+ "Address": "Token",
1796
+ "Cookie": "Token"
1797
+ },
1798
+ "Destination": "AuthResponse"
1799
+ },
1800
+ "fetch-observations": {
1801
+ "GUIDTask": "fetch-observations",
1802
+ "Name": "Fetch Observations Page One",
1803
+ "Type": "RestRequest",
1804
+ "URL": "https://api.example.com/1.0/ObservationsFilter/0/25",
1805
+ "Method": "POST",
1806
+ "Body": {
1807
+ "IDProject": 8605,
1808
+ "MatchAllTags": false,
1809
+ "IDAuthor": [15124],
1810
+ "ObservationType": ["Narrative", "Image", "File"]
1811
+ },
1812
+ "Destination": "ObservationsPageOne"
1813
+ },
1814
+ "save-observations": {
1815
+ "GUIDTask": "save-observations",
1816
+ "Name": "Save Observations to Staging",
1817
+ "Type": "WriteJSON",
1818
+ "File": "ObservationsPageOne.json",
1819
+ "Address": "ObservationsPageOne"
1820
+ }
1821
+ },
1822
+ "Operations": {
1823
+ "fetch-observations": {
1824
+ "GUIDOperation": "fetch-observations",
1825
+ "Name": "Authenticate and Fetch Observations",
1826
+ "Tasks": [
1827
+ "authenticate",
1828
+ "fetch-observations",
1829
+ "save-observations"
1830
+ ]
1831
+ }
1832
+ }
1833
+ }
1834
+ ```
1835
+
1836
+ When `fetch-observations` executes:
1837
+
1838
+ 1. **authenticate** -- POSTs credentials to the login endpoint. Any
1839
+ `Set-Cookie` response headers are automatically captured into
1840
+ `GlobalState.Cookies`. Additionally, `CaptureToken` extracts
1841
+ the `Token` field from the JSON response body and stores it as
1842
+ `GlobalState.Cookies.Token`. The full JSON response is stored at
1843
+ `GlobalState.AuthResponse`.
1844
+
1845
+ 2. **fetch-observations** -- POSTs the filter criteria. The shared
1846
+ cookie jar already contains the session token from step 1, so it
1847
+ is automatically included in this request's `Cookie` header.
1848
+ The response is stored at `GlobalState.ObservationsPageOne`.
1849
+
1850
+ 3. **save-observations** -- Uses `Address` to resolve
1851
+ `GlobalState.ObservationsPageOne` and writes it as JSON to
1852
+ `ObservationsPageOne.json` in the operation's staging folder.
1853
+
1854
+ No explicit cookie configuration is needed between steps -- the
1855
+ `RestRequest` shared cookie jar handles it automatically. The
1856
+ combination of `Set-Cookie` capture and `CaptureToken` covers both
1857
+ cookie-based and token-based authentication flows.