@enfocussw/switch-scripting-context 0.1.0

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 (33) hide show
  1. package/CHANGELOG.md +504 -0
  2. package/README.md +192 -0
  3. package/bin/cli.js +8 -0
  4. package/dist/init.d.ts +78 -0
  5. package/dist/init.js +894 -0
  6. package/docs/switch-api/api-versions.md +127 -0
  7. package/docs/switch-api/connection.md +82 -0
  8. package/docs/switch-api/document-classes.md +189 -0
  9. package/docs/switch-api/entry-points.md +185 -0
  10. package/docs/switch-api/enums.md +181 -0
  11. package/docs/switch-api/execution-environment.md +143 -0
  12. package/docs/switch-api/flow-element.md +143 -0
  13. package/docs/switch-api/http.md +96 -0
  14. package/docs/switch-api/job-patterns.md +238 -0
  15. package/docs/switch-api/job.md +187 -0
  16. package/docs/switch-api/logging.md +117 -0
  17. package/docs/switch-api/switch.md +210 -0
  18. package/docs/switch-appstore/app-guidelines.md +281 -0
  19. package/docs/switch-appstore/app-manual.md +84 -0
  20. package/docs/switch-appstore/app-store-listing.md +69 -0
  21. package/docs/switch-appstore/app-store-submission.md +83 -0
  22. package/docs/switch-project/debugging.md +61 -0
  23. package/docs/switch-project/logs-and-dataroot.md +80 -0
  24. package/docs/switch-project/node-versions.md +87 -0
  25. package/docs/switch-project/project-planning.md +149 -0
  26. package/docs/switch-project/property-documentation.md +75 -0
  27. package/docs/switch-project/property-editors.md +249 -0
  28. package/docs/switch-project/script-declaration.md +407 -0
  29. package/docs/switch-project/script-structure.md +157 -0
  30. package/docs/switch-project/tooling.md +165 -0
  31. package/docs/switch-project/vscode.md +90 -0
  32. package/docs/switch-scripting.md +70 -0
  33. package/package.json +65 -0
@@ -0,0 +1,238 @@
1
+ ---
2
+ id: job-patterns
3
+ category: switch-api
4
+ order: 16
5
+ summary: "`Job` **rules and gotchas**: file access semantics, temp cleanup, which `sendTo*()` suits which connection type, dataset/child ordering, locking shared global data, executor limits"
6
+ triggers:
7
+ - "Writing or reviewing job-handling code. Pair with `job.md` when you also need signatures"
8
+ ---
9
+
10
+ # Job Patterns and Behavioral Rules
11
+
12
+ Common behavioral rules for file access, routing, temp file cleanup, and child jobs. These are the most frequent sources of bugs.
13
+
14
+ ---
15
+
16
+ <!-- docs-index:contents begin -->
17
+ ## Contents
18
+
19
+ - File access semantics
20
+ - Temp file cleanup
21
+ - Platform-independent paths
22
+ - Sending jobs
23
+ - Dataset writes must precede child job creation
24
+ - processLater
25
+ - Shared state in global data
26
+ - Executor cleanup thresholds
27
+ - Driving a third-party CLI application
28
+ <!-- docs-index:contents end -->
29
+
30
+ ## File access semantics
31
+
32
+ `job.get(accessLevel)` and `job.getDataset(name, accessLevel)` behave differently depending on the access level:
33
+
34
+ | Access level | What it returns | Modification |
35
+ |---|---|---|
36
+ | `AccessLevel.ReadOnly` | Path in the element's **input folder** | Throws if the file is modified |
37
+ | `AccessLevel.ReadWrite` | Path in a **temp location** (copy) | Auto-uploaded to the output folder when `sendTo*()` is called |
38
+
39
+ Always use `AccessLevel.ReadOnly` unless you need to modify the file content.
40
+
41
+ ---
42
+
43
+ ## Temp file cleanup
44
+
45
+ Switch does **not** automatically clean up files you create or pass to job-creation methods. The script is responsible:
46
+
47
+ - Files/folders passed to `flowElement.createJob(path)` — delete after routing.
48
+ - Files/folders passed to `job.createChild(path)` — delete after routing.
49
+ - Files/folders passed to `job.createDataset(name, filePath, model)` — delete after routing.
50
+
51
+ Use `flowElement.createPathWithName()` to create temp files by default — its output lives in the executor's temp area, which Switch removes automatically on executor refresh. Even so, delete explicitly after routing rather than relying on that refresh (see below). `createPathWithName()` needs Switch 24.0+; on an older Switch, build a unique path under `os.tmpdir()` and delete it explicitly after routing.
52
+
53
+ If a temp file is created some other way (e.g. `fs`/`path` writing outside the temp area, or a library that writes its own scratch files), the same rule applies: the script must delete it explicitly. Don't rely on Switch's executor-refresh cleanup for files outside the temp area — it only removes what it put there, so anything else creates a permanent leak.
54
+
55
+ The [`tmp`](https://www.npmjs.com/package/tmp) npm package is common in scripts written by app creators, but it is **not recommended** here — prefer `flowElement.createPathWithName()` instead. If `tmp` is used anyway: its `setGracefulCleanup()` is not always reliable in the Switch script execution environment, so don't depend on it for cleanup — delete the file explicitly after routing regardless. Also pass `discardDescriptor: true` when creating a temp path (e.g. `tmp.fileSync({ discardDescriptor: true })`), or the open file descriptor can leak.
56
+
57
+ > Files accumulate between executor refreshes if not cleaned up. The executor is refreshed after 5 min idle, 5,000 tasks, 150 MB memory, or 1,024 open file handles — at which point accumulated files **created via `createPathWithName()`** are removed automatically, but do not rely on this.
58
+
59
+ ---
60
+
61
+ ## Platform-independent paths
62
+
63
+ Switch Server runs on both Windows and macOS. **App guideline (mandatory for apps, recommended for
64
+ scripts):** a script package should work unmodified on either — paths returned by `job.get()`,
65
+ `flowElement.createPathWithName()`, and similar methods use the host OS's native separator, so avoid
66
+ hardcoding `/` or `\` when building or splitting a path, and don't assume one form when comparing or
67
+ matching paths. Use Node's `path` module (`path.join`, `path.resolve`, `path.sep`, `path.basename`,
68
+ etc.) instead of string concatenation.
69
+
70
+ ---
71
+
72
+ ## Sending jobs
73
+
74
+ After calling any `job.sendTo*()`:
75
+ - Only further `sendTo*()` calls and `job.fail()` are allowed on that job object.
76
+ - An **unmodified** incoming job can be sent to multiple connections (call `sendTo*()` multiple times).
77
+ - A **modified** job (ReadWrite access used) must use `job.createChild()` to produce additional copies for routing to other connections.
78
+
79
+ ```ts
80
+ // Route unmodified job to two connections — OK
81
+ await job.sendToData(Connection.Level.Success);
82
+ await job.sendToData(Connection.Level.Warning); // only if job was not modified
83
+
84
+ // Route modified content to multiple connections — use createChild
85
+ const child = await job.createChild(tempPath);
86
+ await job.sendTo(conn1);
87
+ await child.sendTo(conn2);
88
+ // Clean up tempPath after routing
89
+ await fs.promises.rm(tempPath);
90
+ ```
91
+
92
+ **Consistency with declared connections** (applies to every script, not just apps):
93
+ - If `OutgoingConnections` is not `No`, the script must call some `sendTo*()` method at least once
94
+ for any job it actually routes. A purely timer/event-driven script that never touches a job
95
+ (polling an API, rotating logs, etc.) is a legitimate exception.
96
+ - If `OutgoingConnections="Unlimited"` (more than one outgoing connection allowed), never use
97
+ `job.sendToSingle()` — it only makes sense when exactly one outgoing connection exists.
98
+ - If no output is produced for a job, call `job.sendToNull()` rather than leaving it unrouted.
99
+ - `ConnectionType="Move"` (see [script-declaration.md](../switch-project/script-declaration.md#connectionfields)):
100
+ never use `job.sendToData()`/`job.sendToLog()` — those are for `TrafficLight` connections only. Use
101
+ `job.sendTo()`/`job.sendToSingle()`.
102
+ - `ConnectionType="TrafficLight"`: use only `job.sendToData()`/`job.sendToLog()` — never
103
+ `job.sendTo()`/`job.sendToSingle()`. Use `sendToData` when a Data Success/Warning/Error connection
104
+ is enabled in the declaration, and `sendToLog` when the corresponding Log connection is enabled.
105
+ Conversely, don't declare a Success/Warning/Error element the script never routes to — omit it from
106
+ `ConnectionFields` entirely (see the TrafficLight example in
107
+ [script-declaration.md](../switch-project/script-declaration.md#connectionfields)).
108
+ `sendToData(level)` fails the job if no connected outgoing data connection accepts that level,
109
+ and there is no supported way to check in advance which levels are wired up (see
110
+ [connection.md § Connection.Level enum](connection.md#connectionlevel-enum)). If routing at a
111
+ given level is optional rather than guaranteed by the flow design, expose an explicit custom
112
+ property that lets the flow author opt in or out, and call `job.sendToNull()` when opted out
113
+ instead of guessing whether `sendToData()` will succeed.
114
+ - Prefer `job.fail()`/`flowElement.failProcess()` for genuine processing errors (see
115
+ [logging.md](logging.md)) over silently routing a failed job to a generic error connection —
116
+ reserve an error `TrafficLight` connection for cases the flow author should be able to reroute
117
+ downstream, not as a substitute for `fail()`.
118
+
119
+ ---
120
+
121
+ ## Dataset writes must precede child job creation
122
+
123
+ **Known issue — pending a server-side fix; remove this section once fixed.** Applies to all current versions.
124
+
125
+ `job.createChild()` copies the parent's *current* datasets immediately (see [job.md](job.md#child-jobs)); `job.createDataset()` only registers a pending write, flushed to disk at the next `sendTo*()` call (see [job.md](job.md#datasets-metadata)). If a child is created *before* a dataset is written, the child inherits the job's existing dataset under that name instead. At flush time, the pending write fans out concurrently to the parent and to every child created so far, all reading from the same source file on disk — targets that already have a dataset by that name get the file **moved** into place, while targets that don't get it **copied**. When a mix of both happens against a single source file, the move wins the race and the copies fail with errors like `Could not place the file with the decoded data '...' into the datasets folder`, deterministically and on every retry (the source file is gone after the first attempt).
126
+
127
+ **Rule:** perform every dataset write on a job before creating any child of it. Where a script's structure naturally interleaves the two, iterate the dataset outputs first, then the child-job outputs, rather than handling each output in one pass.
128
+
129
+ Approaches that do not work:
130
+ - Deferring `createChild()` until after routing — the SDK rejects any of `createChild`, `setPriority`, `log`, `setPrivateData`, `listDatasets`, `removeDataset`, and others once a `sendTo*()` call has been made on the job, throwing `Method is not allowed at this time. Have you called any sendTo method already?`.
131
+ - Creating children first and only deferring *their* routing — a child is enlisted in the dataset flush at `createChild()` time, not when it's routed.
132
+ - Calling `removeDataset()` before `createDataset()` — this only clears the parent's record and cannot reach copies a child already inherited.
133
+
134
+ A dataset name that's genuinely new to the job never triggers this, since every target takes the copy path — the bug requires a pre-existing dataset of that name.
135
+
136
+ ---
137
+
138
+ ## processLater
139
+
140
+ `job.processLater()` (Switch 22.1+) defers the job for the next `timerFired` invocation.
141
+
142
+ Constraints:
143
+ - Minimum deferral: **10 seconds** from the time `jobArrived` was called.
144
+ - Cannot be called on jobs created with `createJob()` or `createChild()`.
145
+ - Private data changes made before `processLater()` are preserved.
146
+ - New datasets created before `processLater()` are **discarded**.
147
+
148
+ ---
149
+
150
+ ## Shared state in global data
151
+
152
+ The deferred-processing pattern (register each job in global data in `jobArrived`, fetch and
153
+ process the registered jobs in `timerFired` with `flowElement.getJobs(ids)`) is a read-modify-write
154
+ of a shared tag. In `Concurrent` mode several `jobArrived` invocations run at once, so without a lock
155
+ two of them can read the same list and one overwrites the other's addition.
156
+
157
+ Lock the tag, change it, and write it back straight away. Do the slow work (fetching, processing and
158
+ sending jobs) after the lock is released, because every other invocation that reads the tag waits
159
+ for as long as the lock is held. The locking rules this relies on, including the one-lock-per-
160
+ invocation gotcha, are in [switch.md § Locking](switch.md#locking).
161
+
162
+ ```ts
163
+ const TAG = 'pendingJobIds';
164
+
165
+ async function jobArrived(s: Switch, flowElement: FlowElement, job: Job): Promise<void> {
166
+ const stored = await s.getGlobalData(Scope.FlowElement, TAG, true);
167
+ let updated = stored;
168
+ try {
169
+ const ids: string[] = stored === '' ? [] : JSON.parse(stored);
170
+ if (!ids.includes(job.getId())) ids.push(job.getId());
171
+ updated = JSON.stringify(ids);
172
+ } finally {
173
+ await s.setGlobalData(Scope.FlowElement, TAG, updated); // releases the lock, also on error
174
+ }
175
+ }
176
+
177
+ async function timerFired(s: Switch, flowElement: FlowElement): Promise<void> {
178
+ // Take the list and empty it under the lock, then work on the jobs without holding it.
179
+ const stored = await s.getGlobalData(Scope.FlowElement, TAG, true);
180
+ await s.setGlobalData(Scope.FlowElement, TAG, '[]'); // releases the lock
181
+ const ids: string[] = stored === '' ? [] : JSON.parse(stored);
182
+ if (ids.length === 0) return;
183
+ const jobs = await flowElement.getJobs(ids);
184
+ // process and send each job
185
+ }
186
+ ```
187
+
188
+ To track more than one tag, lock them together with the array form of `getGlobalData` and release
189
+ them with one array `setGlobalData`; a second locking call in the same invocation throws. Remove the
190
+ tag in `flowStopTriggered` (global data is never cleaned up automatically), and see the
191
+ [flow restart gotcha](entry-points.md#job-processing) for why `jobArrived` must also skip jobs that
192
+ are already registered.
193
+
194
+ ---
195
+
196
+ ## Executor cleanup thresholds
197
+
198
+ The Node.js executor process is recycled when any of the following thresholds is reached:
199
+
200
+ | Threshold | Value |
201
+ |---|---|
202
+ | Idle time | 5 minutes |
203
+ | Tasks processed | 5,000 |
204
+ | Memory consumption | 150 MB |
205
+ | Open file handles | 1,024 |
206
+
207
+ On cleanup, any files/folders created in the temp area are removed. Always close file handles and database connections before the entry point returns.
208
+
209
+ See [execution-environment.md](execution-environment.md) for how this process model affects state persistence and error handling across job invocations.
210
+
211
+ ## Driving a third-party CLI application
212
+
213
+ `findApplicationPath` and the `ApplicationPath` property are the legacy-scripting mechanism for
214
+ locating a third-party application; neither is available to Node.js scripts, and `ApplicationPath`
215
+ is a reserved name Switch silently drops from the declaration (see
216
+ [script-declaration.md](../switch-project/script-declaration.md#built-in-elementfields)). A Node.js script or
217
+ app that shells out to an external binary resolves the path itself, using the following pattern.
218
+
219
+ 1. Declare an ordinary custom property for the path under a name of your own — `cliPath`, not
220
+ `ApplicationPath`.
221
+ 2. Give it the editor chain `automatic;choosefile;sltextwithvar;scriptexp`, with
222
+ `Default="Automatic"` and `Subtype="automatic"`. The `automatic` literal editor is what lets the
223
+ user express "find it yourself" without typing a magic string, and `getPropertyStringValue()`
224
+ returns the literal `"Automatic"` for it — see
225
+ [property-editors.md § Literal editors](../switch-project/property-editors.md#literal-editors).
226
+ 3. Resolve the path once per flow start, in `flowStartTriggered`: if the property reads
227
+ `Automatic`, run whatever discovery the application needs; otherwise take
228
+ `flowElement.getPropertyStringValue('cliPath')`. Verify the resulting path actually exists, then
229
+ store it in global data at `Scope.FlowElement`. If it doesn't exist, store an empty string
230
+ rather than leaving the tag unset.
231
+ 4. In `jobArrived`/`timerFired`, read the path back from global data. An empty string means
232
+ discovery failed: call `flowElement.failProcess()` so the element goes into an error state,
233
+ instead of failing every individual job.
234
+ 5. Remove the global data entry in `flowStopTriggered` — global data is never cleaned up
235
+ automatically (see [switch.md § Global data](switch.md#global-data)).
236
+
237
+ Doing discovery once at flow start rather than per job keeps a filesystem search off the job path,
238
+ and gives the operator a single clear element-level error when the application is missing.
@@ -0,0 +1,187 @@
1
+ ---
2
+ id: job
3
+ category: switch-api
4
+ order: 5
5
+ summary: "`Job` **signatures**: routing, file access, child jobs, private data, datasets"
6
+ triggers:
7
+ - "Looking up what a `job.*` method takes, returns, or throws"
8
+ ---
9
+
10
+ # Job Class
11
+
12
+ A `Job` represents a file or folder moving through the flow. It is passed to `jobArrived` and can be created via `flowElement.createJob()` or `job.createChild()`. Every job must be routed with a `sendTo*()` call or `fail()` before the entry point ends (or at a later time via `timerFired`).
13
+ > Jobs obtained via `flowElement.getJobs()` throw on `getPrivateData()`, `listDatasets()`, and `getDataset()` in the same entry point invocation — private data/metadata cannot be *read* back. `setPrivateData()`/`removePrivateData()` are not restricted this way.
14
+
15
+ <!-- docs-index:contents begin -->
16
+ ## Contents
17
+
18
+ - Identity
19
+ - Priority
20
+ - File access
21
+ - Routing
22
+ - Failure & logging
23
+ - Child jobs
24
+ - Private data
25
+ - Datasets (metadata)
26
+ - Variables & structured data
27
+ <!-- docs-index:contents end -->
28
+
29
+ ## Identity
30
+
31
+ ```ts
32
+ job.getName(includeExtension?: boolean): string // default: true
33
+ ```
34
+ Returns the filename without the internal ID prefix. Optionally strips the extension.
35
+
36
+ ```ts
37
+ job.getId(): string
38
+ ```
39
+ Returns the unique job ID (the prefix portion of the filename, without underscores).
40
+
41
+ ```ts
42
+ job.isFile(): boolean
43
+ job.isFolder(): boolean
44
+ ```
45
+
46
+ ## Priority
47
+
48
+ ```ts
49
+ // Switch 22.1+
50
+ job.getPriority(): number
51
+ job.setPriority(priority: number): void
52
+ ```
53
+ Use `Priority.*` enum values for standard levels.
54
+
55
+ ## File access
56
+
57
+ ```ts
58
+ job.get(accessLevel: AccessLevel): Promise<string>
59
+ ```
60
+ Returns the local filesystem path to the job. Use `AccessLevel.ReadOnly` to read, `AccessLevel.ReadWrite` if you will modify the file/folder. Modified content is automatically uploaded on the next `sendTo*()` call. Throws if `ReadOnly` is used but the file was modified.
61
+
62
+ ## Routing
63
+
64
+ Every job must be routed exactly once. See [job-patterns.md § Sending jobs](job-patterns.md#sending-jobs) for which `sendTo*()` method is appropriate for a given `ConnectionType`/`OutgoingConnections` declaration.
65
+
66
+ **Routing is applied when the entry point returns.** Each `sendTo*()` call registers a routing action; Switch places all the jobs from one invocation in their output folders together, after it returns. If the entry point throws or is aborted, none of its routing actions reach Switch, including those for jobs routed earlier in the same call. A long `timerFired` that produces many jobs delivers them all at the end; to deliver them sooner, handle fewer items per call and let the timer fire again.
67
+
68
+ ```ts
69
+ job.sendToNull(): Promise<void>
70
+ ```
71
+ Discard the job (mark as completed with no output).
72
+
73
+ ```ts
74
+ job.sendToSingle(newName?: string): Promise<void>
75
+ ```
76
+ Send to the single outgoing move connection. Optionally rename.
77
+
78
+ ```ts
79
+ job.sendTo(connection: Connection, newName?: string): Promise<void>
80
+ ```
81
+ Send to a specific connection (any type). Get connections via `flowElement.getOutConnections()`.
82
+
83
+ ```ts
84
+ job.sendToData(level: Connection.Level, newName?: string): Promise<void>
85
+ ```
86
+ Send via a traffic light "data" connection at the specified level. Fails the job if no matching connection exists. **There is no supported way to check in advance whether a connection at that level exists** — don't call `connection.getPropertyStringValue("Success")`/`"Warning"`/`"Error"` expecting to detect this; that throws an invalid-tag error, since traffic light levels aren't a readable connection property. See [job-patterns.md § Sending jobs](job-patterns.md#sending-jobs) for the pattern (an explicit custom property plus `job.sendToNull()`).
87
+
88
+ ```ts
89
+ job.sendToLog(level: Connection.Level, model: DatasetModel, newName?: string): Promise<void>
90
+ ```
91
+ Send via a traffic light "log" connection. For "data with log" connections, attaches the job as a metadata dataset to data jobs routed via `sendToData`. Discards the job if no matching connection exists.
92
+
93
+ ```ts
94
+ // Switch 22.1+
95
+ job.sendToChannel(channelId: string, newName?: string): Promise<void>
96
+ ```
97
+ Send to a named channel. Unlike `sendToData`/`sendToLog`, this **throws** synchronously (it does not fail-and-route the job) if the channel has no active subscriber, or if `channelId`/`newName` is empty — catch it explicitly.
98
+
99
+ ```ts
100
+ // Switch 22.1+
101
+ job.processLater(seconds?: number): Promise<void> // default: 300
102
+ ```
103
+ Re-queue the job for `jobArrived` after a minimum delay (re-evaluates dynamic properties). Minimum delay: 10 seconds from `jobArrived` (a warning is logged if less). Cannot be called on newly created or child jobs. Content is not modified; newly created datasets are discarded. Private data changes are preserved.
104
+
105
+ ## Failure & logging
106
+
107
+ See [logging.md](logging.md) for log level semantics and logging practice.
108
+
109
+ ```ts
110
+ job.fail(message: string, messageParams?: (string | number | boolean)[]): void
111
+ ```
112
+ Log a fatal error and move the job to Problem Jobs. Newly created jobs are not moved. Use `%1`, `%2`, etc. for substitution. When targeting Switch 20.x, always pass `messageParams`, even as `[]` (see [api-versions.md](api-versions.md#declaration-changes-to-existing-methods)).
113
+
114
+ ```ts
115
+ job.log(level: LogLevel, message: string, messageParams?: (string | number | boolean)[]): Promise<void>
116
+ ```
117
+ Log a message including job context. To log a literal `%`, pass it as a param: `job.log(LogLevel.Info, '%1', [message])`.
118
+
119
+ ## Child jobs
120
+
121
+ ```ts
122
+ job.createChild(path: string): Promise<Job>
123
+ ```
124
+ Creates a new job inheriting the processing history, metadata, and private data of the parent. Takes effect immediately: the child's datasets are copied from the parent's *current* server-side state, so any pending `createDataset()` write not yet flushed by a `sendTo*()` call is not reflected in it. See [job-patterns.md](job-patterns.md#dataset-writes-must-precede-child-job-creation) — creating a child before writing a dataset is a common source of bugs. The caller is responsible for cleaning up the source file after routing (see [job-patterns.md](job-patterns.md#temp-file-cleanup)).
125
+
126
+ ## Private data
127
+
128
+ Private data is arbitrary key/value storage attached to a job and passed along with it. `EnfocusSwitchPrivateDataTag` needs Switch 21.0+; before 21.0, store string values only (see [api-versions.md](api-versions.md#declaration-changes-to-existing-methods)). For each tag's string value (`"EnfocusSwitch.<name>"`) and the release that added it, see [enums.md § EnfocusSwitchPrivateDataTag](enums.md#enfocusswitchprivatedatatag).
129
+
130
+ ```ts
131
+ job.getPrivateData(tag: string | EnfocusSwitchPrivateDataTag): Promise<any>
132
+ job.getPrivateData(tags?: (string | EnfocusSwitchPrivateDataTag)[]): Promise<{ tag: string, value: any }[]>
133
+ ```
134
+ Read one tag (returns the value) or multiple tags / all tags (returns an array). Returns an empty string for missing tags.
135
+
136
+ ```ts
137
+ job.setPrivateData(tag: string | EnfocusSwitchPrivateDataTag, value: any): Promise<void>
138
+ job.setPrivateData(privateData: { tag: string | EnfocusSwitchPrivateDataTag, value: any }[]): Promise<void>
139
+ ```
140
+ Write one or multiple private data entries. Replaces existing values.
141
+
142
+ ```ts
143
+ job.removePrivateData(tag: string | EnfocusSwitchPrivateDataTag): Promise<void>
144
+ job.removePrivateData(tags: (string | EnfocusSwitchPrivateDataTag)[]): Promise<void>
145
+ ```
146
+ Delete one or multiple private data entries.
147
+
148
+ ## Datasets (metadata)
149
+
150
+ Datasets are named files (XML, XMP, JDF, JSON, or Opaque) attached to a job. `DatasetModel.JSON` needs Switch 22.0+; read a JSON dataset back with `job.getJSONData()` (24.0+).
151
+
152
+ ```ts
153
+ job.listDatasets(): Promise<{ name: string, model: DatasetModel, extension: string }[]>
154
+ ```
155
+ List all datasets attached to the job. **Immediate**: reads server state at once, so a pending `createDataset()` write not yet flushed by `sendTo*()` will not appear.
156
+
157
+ ```ts
158
+ job.createDataset(name: string, filePath: string, model: DatasetModel): Promise<void>
159
+ ```
160
+ Attach a new dataset. **Deferred**: registers in-memory only; the file is not uploaded until the next `sendTo*()` call. Caller must clean up the source file after routing — see [job-patterns.md](job-patterns.md#temp-file-cleanup).
161
+
162
+ ```ts
163
+ job.getDataset(name: string, accessLevel: AccessLevel): Promise<string>
164
+ ```
165
+ Returns the local path to the dataset file. Prefer `AccessLevel.ReadOnly`.
166
+
167
+ ```ts
168
+ job.removeDataset(name: string): Promise<void>
169
+ ```
170
+ Remove a dataset by name. **Immediate**: updates server state at once. Throws if it does not exist.
171
+
172
+ ## Variables & structured data
173
+
174
+ Everything in this section needs Switch 24.0+.
175
+
176
+ ```ts
177
+ job.getVariableAsString(variable: string): Promise<string>
178
+ ```
179
+ Returns the value of a Switch variable as a string.
180
+
181
+ ```ts
182
+ job.getxmlData(path: any, xpathQuery: any): Promise<any>
183
+ job.getxmpData(path: any, xpath: any): Promise<any>
184
+ job.getJdfData(path: any, xpath: any): Promise<any>
185
+ job.getJSONData(metadata: any): Promise<any>
186
+ ```
187
+ Convenience helpers to read XML/XMP/JDF/JSON data from a dataset path and return it as a parsed object. On error, these log the failure and resolve to `undefined` rather than throwing or rejecting — check the result rather than wrapping the call in `try`/`catch`.
@@ -0,0 +1,117 @@
1
+ ---
2
+ id: logging
3
+ category: switch-api
4
+ order: 18
5
+ summary: "Log level semantics, logging practice, `console.log` limitation, common gotchas"
6
+ triggers:
7
+ - "Adding or reviewing `job.log()`/`flowElement.log()`/`job.fail()`/`failProcess()` calls"
8
+ ---
9
+
10
+ # Logging Practices
11
+
12
+ Guidance for using `job.log()`, `flowElement.log()`, `job.fail()`, and `flowElement.failProcess()`
13
+ — see [job.md](job.md#failure--logging) and
14
+ [flow-element.md](flow-element.md#logging) for their signatures. `LogLevel` values are
15
+ listed in [enums.md](enums.md). Each log call has a small performance overhead, so where
16
+ and how often to log is a deliberate choice, not a default.
17
+
18
+ <!-- docs-index:contents begin -->
19
+ ## Contents
20
+
21
+ - Log levels
22
+ - Don't add a verbosity property
23
+ - Logging in loops
24
+ - `console.log` does not work
25
+ - Don't double-log before a failure
26
+ - Message formatting and the `%` gotcha
27
+ - Signature gotcha: array vs. single value
28
+ - Placeholders instead of concatenation
29
+ - Message quality
30
+ - Log volume
31
+ <!-- docs-index:contents end -->
32
+
33
+ ## Log levels
34
+
35
+ | Level | Use for |
36
+ |---|---|
37
+ | `Error` | The job/step failed or produced a wrong/unusable result. Often accompanies or precedes a hard stop, but can stand alone if the job continues in a degraded way. |
38
+ | `Warning` | Something unexpected happened but processing continued successfully (a fallback was used, an optional resource was missing). |
39
+ | `Info` | Normal, expected checkpoints worth surfacing to a flow operator without them needing debug mode (e.g. "processed 40 records", "output written to X"). |
40
+ | `Debug` | Verbose detail for diagnosing execution. Many users leave debug logging enabled in production to self-diagnose issues and to help app developers and Enfocus Support — leave meaningful checkpoints in, but trim noisy or no-longer-useful debug logs before shipping. **`Debug` messages only reach the log database if the Switch preference "Log debug messages" is turned on** (off by default) — see [logs-and-dataroot.md](../switch-project/logs-and-dataroot.md#retention-and-the-debug-level-gate). |
41
+
42
+ ## Don't add a verbosity property
43
+
44
+ Switch already has the switch: `Debug` messages only reach the log database when the preference
45
+ "Log debug messages" is on. A script-level "log detail" or "verbose logging" property duplicates a
46
+ control the user already has, adds a row to the property pane, and (for an app) adds strings to
47
+ translate. Put verbose detail behind `LogLevel.Debug` and let the preference govern it.
48
+
49
+ ## Logging in loops
50
+
51
+ Don't categorically avoid logging inside loops — per-item logs can matter (e.g. a per-job error).
52
+ But where multiple log calls can be combined without losing clarity, prefer a single summary over
53
+ one call per iteration, e.g. `"Found 12 matching jobs"` instead of one log line per job.
54
+
55
+ ## `console.log` does not work
56
+
57
+ `console.log` only produces output when a debug session is attached (see
58
+ [debugging.md](../switch-project/debugging.md)); in a normal run it is not visible anywhere. Never rely on it
59
+ in production scripts — use `job.log()`/`flowElement.log()` for anything that should be visible in
60
+ the log/message pane.
61
+
62
+ ## Don't double-log before a failure
63
+
64
+ `job.fail()` and `flowElement.failProcess()` already log their message as a fatal error. Don't call
65
+ `job.log(LogLevel.Error, ...)` / `flowElement.log(LogLevel.Error, ...)` with the same message
66
+ immediately before them — it duplicates the entry in the log. Use a separate `Error` log call only
67
+ when it conveys something distinct from the failure message.
68
+
69
+ ## Message formatting and the `%` gotcha
70
+
71
+ Template literals are fine for ordinary log messages. But a string that may contain a literal `%`
72
+ character — commonly external/untrusted values such as cloud storage URLs — will error if
73
+ interpolated directly into the message, because it gets parsed as a `%1`-style substitution token.
74
+ Workaround: pass `'%1'` as the message and the value as the parameter, e.g.:
75
+
76
+ ```ts
77
+ job.log(LogLevel.Info, '%1', [urlThatMightContainPercent]);
78
+ ```
79
+
80
+ ## Signature gotcha: array vs. single value
81
+
82
+ `job.fail(message, messageParams?: (string | number | boolean)[])` takes an **array**.
83
+ `flowElement.failProcess(message, messageParam?: string | number | boolean)` takes a **single
84
+ value**, not an array. Passing an array to `failProcess` is a common mistake.
85
+
86
+ ## Placeholders instead of concatenation
87
+
88
+ Build log messages with `%1`, `%2`, … placeholders and pass the dynamic values as
89
+ `messageParams`/`messageParam`, rather than concatenating or interpolating them into the message
90
+ string (beyond the literal-`%` escaping case above). A concatenated message can't be translated
91
+ correctly — word order and pluralization vary by language, so a translator needs the message
92
+ template and the substituted values kept separate. This matters even for a script that only ships in
93
+ English today if it might ever be localized later.
94
+
95
+ ```ts
96
+ // Avoid — can't be translated correctly, and hits the literal-% gotcha if fileName contains one
97
+ job.log(LogLevel.Info, `Processed file ${fileName} in ${seconds}s`);
98
+
99
+ // Prefer
100
+ job.log(LogLevel.Info, 'Processed file %1 in %2s', [fileName, seconds]);
101
+ ```
102
+
103
+ ## Message quality
104
+
105
+ - Log messages are user-facing text — proofread them. A message with grammatical errors or typos
106
+ looks unfinished and undermines trust.
107
+ - Log an `Error` when a custom `validateProperties`/`validateConnectionProperties` check returns
108
+ `valid: false` for a tag, and when `getLibraryForProperty`/`getLibraryForConnectionProperty` would
109
+ otherwise return an empty list — see [entry-points.md § Property UI callbacks](entry-points.md#property-ui-callbacks).
110
+ Silently returning `false`/`[]` with no log line leaves the user without any explanation.
111
+
112
+ ## Log volume
113
+
114
+ **App guideline (mandatory for apps, recommended for scripts):** don't over-log. Beyond the
115
+ [Logging in loops](#logging-in-loops) guidance, avoid `Info`/`Debug` calls that don't help a flow
116
+ operator or support engineer understand what happened — every extra message adds noise (and
117
+ translation burden, for an app) without adding signal.