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.
- package/.babelrc +6 -0
- package/.browserslistrc +1 -0
- package/.browserslistrc-BACKUP +1 -0
- package/.gulpfile-quackage-config.json +7 -0
- package/.gulpfile-quackage.js +2 -0
- package/CONTRIBUTING.md +50 -0
- package/README.md +34 -0
- package/debug/Harness.js +2 -1
- package/docs/.nojekyll +0 -0
- package/docs/_sidebar.md +18 -0
- package/docs/_topbar.md +7 -0
- package/docs/architecture.md +103 -0
- package/docs/cover.md +15 -0
- package/docs/features/api.md +230 -0
- package/docs/features/cli.md +182 -0
- package/docs/features/configuration.md +245 -0
- package/docs/features/manifests.md +177 -0
- package/docs/features/operations.md +292 -0
- package/docs/features/scheduling.md +179 -0
- package/docs/features/tasks.md +1857 -0
- package/docs/index.html +39 -0
- package/docs/overview.md +75 -0
- package/docs/quickstart.md +167 -0
- package/docs/retold-catalog.json +24 -0
- package/docs/retold-keyword-index.json +19 -0
- package/package.json +5 -2
- package/source/Ultravisor.cjs +2 -2
- package/source/cli/Ultravisor-CLIProgram.cjs +38 -0
- package/source/cli/commands/Ultravisor-Command-ScheduleOperation.cjs +26 -2
- package/source/cli/commands/Ultravisor-Command-ScheduleTask.cjs +26 -2
- package/source/cli/commands/Ultravisor-Command-ScheduleView.cjs +22 -0
- package/source/cli/commands/Ultravisor-Command-SingleOperation.cjs +49 -1
- package/source/cli/commands/Ultravisor-Command-SingleTask.cjs +51 -1
- package/source/cli/commands/Ultravisor-Command-Stop.cjs +4 -0
- package/source/cli/commands/Ultravisor-Command-UpdateTask.cjs +91 -0
- package/source/config/Ultravisor-Default-Command-Configuration.cjs +6 -1
- package/source/services/Ultravisor-Hypervisor-Event-Base.cjs +18 -1
- package/source/services/Ultravisor-Hypervisor-State.cjs +213 -0
- package/source/services/Ultravisor-Hypervisor.cjs +225 -1
- package/source/services/Ultravisor-Operation-Manifest.cjs +150 -1
- package/source/services/Ultravisor-Operation.cjs +190 -1
- package/source/services/Ultravisor-Task.cjs +339 -1
- package/source/services/events/Ultravisor-Hypervisor-Event-Cron.cjs +71 -1
- package/source/services/tasks/Ultravisor-Task-Base.cjs +264 -0
- package/source/services/tasks/Ultravisor-Task-CollectValues.cjs +188 -0
- package/source/services/tasks/Ultravisor-Task-Command.cjs +65 -0
- package/source/services/tasks/Ultravisor-Task-CommandEach.cjs +190 -0
- package/source/services/tasks/Ultravisor-Task-Conditional.cjs +104 -0
- package/source/services/tasks/Ultravisor-Task-DateWindow.cjs +72 -0
- package/source/services/tasks/Ultravisor-Task-GeneratePagedOperation.cjs +336 -0
- package/source/services/tasks/Ultravisor-Task-LaunchOperation.cjs +143 -0
- package/source/services/tasks/Ultravisor-Task-LaunchTask.cjs +146 -0
- package/source/services/tasks/Ultravisor-Task-LineMatch.cjs +158 -0
- package/source/services/tasks/Ultravisor-Task-Request.cjs +56 -0
- package/source/services/tasks/Ultravisor-Task-Solver.cjs +89 -0
- package/source/services/tasks/Ultravisor-Task-TemplateString.cjs +93 -0
- package/source/services/tasks/rest/Ultravisor-Task-GetBinary.cjs +127 -0
- package/source/services/tasks/rest/Ultravisor-Task-GetJSON.cjs +119 -0
- package/source/services/tasks/rest/Ultravisor-Task-GetText.cjs +109 -0
- package/source/services/tasks/rest/Ultravisor-Task-GetXML.cjs +112 -0
- package/source/services/tasks/rest/Ultravisor-Task-RestRequest.cjs +499 -0
- package/source/services/tasks/rest/Ultravisor-Task-SendJSON.cjs +150 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-CopyFile.cjs +110 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ListFiles.cjs +89 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadBinary.cjs +87 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadJSON.cjs +67 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadText.cjs +66 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-ReadXML.cjs +69 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteBinary.cjs +95 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteJSON.cjs +96 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteText.cjs +99 -0
- package/source/services/tasks/stagingfiles/Ultravisor-Task-WriteXML.cjs +102 -0
- package/source/web_server/Ultravisor-API-Server.cjs +463 -3
- package/test/Ultravisor_tests.js +6097 -1
- package/webinterface/.babelrc +6 -0
- package/webinterface/.browserslistrc +1 -0
- package/webinterface/.browserslistrc-BACKUP +1 -0
- package/webinterface/.gulpfile-quackage-config.json +7 -0
- package/webinterface/.gulpfile-quackage.js +2 -0
- package/webinterface/css/ultravisor.css +121 -0
- package/webinterface/html/index.html +32 -0
- package/webinterface/package.json +39 -0
- package/webinterface/source/Pict-Application-Ultravisor-Configuration.json +15 -0
- package/webinterface/source/Pict-Application-Ultravisor.js +414 -0
- package/webinterface/source/providers/PictRouter-Ultravisor-Configuration.json +42 -0
- package/webinterface/source/views/PictView-Ultravisor-BottomBar.js +65 -0
- package/webinterface/source/views/PictView-Ultravisor-Dashboard.js +236 -0
- package/webinterface/source/views/PictView-Ultravisor-Layout.js +83 -0
- package/webinterface/source/views/PictView-Ultravisor-ManifestList.js +273 -0
- package/webinterface/source/views/PictView-Ultravisor-OperationEdit.js +243 -0
- package/webinterface/source/views/PictView-Ultravisor-OperationList.js +141 -0
- package/webinterface/source/views/PictView-Ultravisor-Schedule.js +280 -0
- package/webinterface/source/views/PictView-Ultravisor-TaskEdit.js +220 -0
- package/webinterface/source/views/PictView-Ultravisor-TaskList.js +248 -0
- package/webinterface/source/views/PictView-Ultravisor-TimingView.js +420 -0
- 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.
|