@cxtms/cx-schema 1.9.67 → 1.9.241
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/dist/cli.js +43 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/schemas/actions/all.json +5 -1
- package/schemas/actions/reload.json +17 -0
- package/schemas/actions/setSessionStorage.json +38 -0
- package/schemas/actions/sound.json +39 -0
- package/schemas/actions/vibrate.json +48 -0
- package/schemas/components/avatar.json +1 -0
- package/schemas/components/badge.json +79 -0
- package/schemas/components/barcodeScanner.json +28 -0
- package/schemas/components/camera.json +177 -0
- package/schemas/components/card.json +78 -0
- package/schemas/components/dashboard-widget.json +39 -0
- package/schemas/components/dashboard.json +46 -0
- package/schemas/components/dataGrid.json +15 -0
- package/schemas/components/field-collection.json +86 -2
- package/schemas/components/field.json +39 -4
- package/schemas/components/form.json +4 -0
- package/schemas/components/index.json +17 -1
- package/schemas/components/infoLine.json +1 -0
- package/schemas/components/map.json +167 -0
- package/schemas/components/module.json +32 -0
- package/schemas/components/planner.json +1 -0
- package/schemas/components/progressBar.json +1 -0
- package/schemas/components/timeline.json +216 -74
- package/schemas/components/timelineGrid.json +5 -1
- package/schemas/fields/autocomplete-googleplaces.json +2 -2
- package/schemas/fields/datetime.json +2 -2
- package/schemas/fields/index.json +4 -0
- package/schemas/fields/number-select.json +88 -0
- package/schemas/fields/rangedatetime.json +10 -2
- package/schemas/fields/select-async.json +2 -2
- package/schemas/fields/toggle.json +91 -0
- package/schemas/schema.graphql +11016 -12171
- package/schemas/schemas.json +131 -4
- package/schemas/workflows/flow/entity.json +3 -1
- package/schemas/workflows/input.json +1 -1
- package/schemas/workflows/output.json +1 -1
- package/schemas/workflows/tasks/all.json +9 -0
- package/schemas/workflows/tasks/commodity.json +8 -0
- package/schemas/workflows/tasks/contact-address.json +25 -0
- package/schemas/workflows/tasks/dispatch-routes.json +64 -0
- package/schemas/workflows/tasks/edi.json +15 -0
- package/schemas/workflows/tasks/export.json +21 -0
- package/schemas/workflows/tasks/httpRequest.json +4 -0
- package/schemas/workflows/tasks/import.json +5 -0
- package/schemas/workflows/tasks/order-move.json +38 -0
- package/schemas/workflows/tasks/order.json +6 -0
- package/schemas/workflows/tasks/organization-config.json +101 -0
- package/schemas/workflows/tasks/tracking-event.json +11 -3
- package/schemas/workflows/tasks/workflow-execute.json +4 -0
- package/schemas/workflows/variable.json +2 -2
- package/schemas/workflows/workflow.json +39 -2
- package/skills/cxtms-developer/SKILL.md +5 -2
- package/skills/cxtms-developer/ref-cli-auth.md +2 -0
- package/skills/cxtms-developer/ref-entity-commodity.md +41 -5
- package/skills/cxtms-developer/ref-entity-contact.md +46 -0
- package/skills/cxtms-developer/ref-entity-dispatch-routing.md +195 -0
- package/skills/cxtms-developer/ref-entity-geography.md +26 -1
- package/skills/cxtms-developer/ref-entity-job.md +6 -2
- package/skills/cxtms-developer/ref-entity-order-sub.md +1 -1
- package/skills/cxtms-developer/ref-entity-order.md +36 -3
- package/skills/cxtms-developer/ref-entity-organization.md +4 -2
- package/skills/cxtms-developer/ref-entity-shared.md +79 -3
- package/skills/cxtms-developer/ref-graphql-query.md +246 -4
- package/skills/cxtms-module-builder/SKILL.md +33 -6
- package/skills/cxtms-module-builder/ref-components-data.md +85 -0
- package/skills/cxtms-module-builder/ref-components-display.md +282 -8
- package/skills/cxtms-module-builder/ref-components-forms.md +386 -21
- package/skills/cxtms-module-builder/ref-components-interactive.md +2 -0
- package/skills/cxtms-module-builder/ref-components-layout.md +288 -29
- package/skills/cxtms-module-builder/ref-components-specialized.md +168 -20
- package/skills/cxtms-workflow-builder/SKILL.md +45 -6
- package/skills/cxtms-workflow-builder/ref-entity.md +100 -4
- package/skills/cxtms-workflow-builder/ref-expressions-ncalc.md +35 -2
- package/skills/cxtms-workflow-builder/ref-expressions-template.md +15 -1
- package/skills/cxtms-workflow-builder/ref-flow.md +6 -1
- package/skills/cxtms-workflow-builder/ref-query.md +16 -0
- package/skills/cxtms-workflow-builder/ref-utilities.md +32 -2
- package/templates/workflow-public-api.yaml +1 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
- Dashboard component
|
|
7
7
|
- DashboardWidget component
|
|
8
8
|
- Widget component
|
|
9
|
+
- Map component
|
|
9
10
|
- Timeline component
|
|
10
11
|
- TimelineGrid component
|
|
11
12
|
- OAuth2 component
|
|
@@ -103,6 +104,55 @@ props:
|
|
|
103
104
|
|
|
104
105
|
---
|
|
105
106
|
|
|
107
|
+
## map
|
|
108
|
+
|
|
109
|
+
Inline Google Maps component for plotting explicit markers or dispatch route/template stops.
|
|
110
|
+
|
|
111
|
+
**Props:**
|
|
112
|
+
| Prop | Type | Default | Description |
|
|
113
|
+
|------|------|---------|-------------|
|
|
114
|
+
| `type` | `google` | `google` | Map provider; only Google Maps is supported |
|
|
115
|
+
| `height` | `number` | `520` | Map viewport height in pixels |
|
|
116
|
+
| `center` | `{lat,lng}` | auto | Optional fixed center; numbers or numeric strings |
|
|
117
|
+
| `zoom` | `number` | auto | Initial zoom; single marker defaults to 14 |
|
|
118
|
+
| `markers` | `array` | - | Explicit markers; takes precedence over `stops` |
|
|
119
|
+
| `stops` | `array` | - | Dispatch route/template stops converted from `stopContact.shipping` |
|
|
120
|
+
| `fitBounds` | `boolean` | `true` | Fit viewport to valid markers when no fixed center is supplied |
|
|
121
|
+
| `showUnmappedNote` | `boolean` | `true` | Show a caption for unplotted markers/stops |
|
|
122
|
+
| `showInfoWindow` | `boolean` | `true` | Show marker label/address/stop type in a Google info window |
|
|
123
|
+
| `onMarkerClick` | `action[]` | - | Fired with clicked marker as `result` |
|
|
124
|
+
|
|
125
|
+
`markers` should include `id`, `label`, `lat`, `lng`, and optionally `number`, `address`, `stopType`. Invalid coordinates and `0,0` are treated as unmapped.
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
component: map
|
|
129
|
+
name: dispatchRouteMap
|
|
130
|
+
props:
|
|
131
|
+
height: 560
|
|
132
|
+
stops: "{{ dispatchRoute.stops }}"
|
|
133
|
+
fitBounds: true
|
|
134
|
+
onMarkerClick:
|
|
135
|
+
- setStore:
|
|
136
|
+
selectedStopId: "{{ result.id }}"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
component: map
|
|
141
|
+
name: terminalMap
|
|
142
|
+
props:
|
|
143
|
+
height: 420
|
|
144
|
+
center: { lat: 33.749, lng: -84.388 }
|
|
145
|
+
zoom: 8
|
|
146
|
+
markers:
|
|
147
|
+
- id: atl
|
|
148
|
+
label: Atlanta Terminal
|
|
149
|
+
lat: 33.749
|
|
150
|
+
lng: -84.388
|
|
151
|
+
address: "Atlanta, GA"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
106
156
|
## notes
|
|
107
157
|
|
|
108
158
|
Rich-text notes/comments component with TipTap editor, message threading, and pagination.
|
|
@@ -177,17 +227,17 @@ CSS Grid-based dashboard with draggable/resizable widgets.
|
|
|
177
227
|
| `options.gridGap` | `number` | `16` | Gap between cells (px) |
|
|
178
228
|
| `options.allowEdit` | `boolean` | `false` | Enable edit mode (drag/resize/add/remove) |
|
|
179
229
|
| `options.showGridLines` | `boolean` | `true` | Grid background in edit mode |
|
|
180
|
-
| `options.autoSave` | `boolean` | `false` | Auto-save layout |
|
|
230
|
+
| `options.autoSave` | `boolean` | `false` | Auto-save layout to UserSettings (`tms:dashboards:{name}:layout`) |
|
|
181
231
|
| `options.title` | `string` | — | Dashboard title |
|
|
182
232
|
| `options.height` | `string` | — | Container height |
|
|
183
233
|
|
|
184
|
-
**Children:** `dashboard-widget` components only.
|
|
234
|
+
**Children:** `dashboard-widget` components only. Saved layouts merge sparse YAML-widget overrides, removed-widget markers, and user-added widget entries back onto YAML defaults.
|
|
185
235
|
|
|
186
236
|
---
|
|
187
237
|
|
|
188
238
|
## dashboard-widget
|
|
189
239
|
|
|
190
|
-
Positioned widget card inside a dashboard. Supports drag-to-move and resize in edit mode.
|
|
240
|
+
Positioned widget card inside a dashboard. Supports drag-to-move and resize in edit mode; the final drag/resize position is what gets persisted.
|
|
191
241
|
|
|
192
242
|
**Props (under `options`):**
|
|
193
243
|
| Prop | Type | Default | Description |
|
|
@@ -280,22 +330,37 @@ Data-driven widget that delegates to sub-components by type.
|
|
|
280
330
|
|
|
281
331
|
## timeline
|
|
282
332
|
|
|
283
|
-
|
|
333
|
+
The mobile renderer supports the shared normal and tracking contracts. In normal mode, `activeTemplate` can override `eventTemplate` for events mapped with `status: active`; both templates receive the event as `item`. Mobile renders horizontal timelines as a scrolling row and does not visually implement `alternateSides` or `showTodayMarker`. Query-backed sources can use static `events` as a failure fallback.
|
|
334
|
+
|
|
335
|
+
MUI Lab Timeline for chronological events or milestone-based tracking progress.
|
|
336
|
+
|
|
337
|
+
**Modes:**
|
|
338
|
+
- `normal` (default): date-range timeline with day/week/month/year view controls.
|
|
339
|
+
- `tracking`: vertical milestone timeline. Events are matched to `milestones[].key` through mapped event `key`; matched milestones are `completed`, unmatched milestones are `pending`, and unmatched events can be shown as `extra`.
|
|
284
340
|
|
|
285
341
|
**Props:**
|
|
286
342
|
| Prop | Type | Default | Description |
|
|
287
343
|
|------|------|---------|-------------|
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
| `
|
|
292
|
-
| `
|
|
293
|
-
| `
|
|
294
|
-
| `
|
|
295
|
-
| `options.
|
|
296
|
-
| `options.
|
|
297
|
-
| `options.
|
|
298
|
-
| `options.
|
|
344
|
+
| `mode` | `normal \| tracking` | `normal` | Timeline behavior |
|
|
345
|
+
| `orientation` | `horizontal \| vertical` | `horizontal` | Layout mode; tracking renders vertical |
|
|
346
|
+
| `view` | `day \| week \| month \| year` | `week` | Time view for normal mode |
|
|
347
|
+
| `startDate` / `endDate` | `string` | current week | Initial date range for normal mode |
|
|
348
|
+
| `eventSources` | `EventSource[]` | — | GraphQL/static event sources |
|
|
349
|
+
| `eventTemplate` | `ComponentProps` | — | Custom event template for normal mode |
|
|
350
|
+
| `milestones` | `Milestone[]` | `[]` | Tracking milestones |
|
|
351
|
+
| `options.height` | `string \| number` | `400`/`auto` | Component height |
|
|
352
|
+
| `options.showTodayMarker` | `boolean` | `true` | Today marker in normal mode |
|
|
353
|
+
| `options.enableZoom` | `boolean` | `true` | View switcher in normal mode |
|
|
354
|
+
| `options.enableNavigation` | `boolean` | `true` | Prev/next/today buttons in normal mode |
|
|
355
|
+
| `options.alternateSides` | `boolean` | `true` | Alternate sides in vertical mode |
|
|
356
|
+
| `options.showTwoColumns` | `boolean` | `true` | Show date/status opposite event content in vertical mode |
|
|
357
|
+
| `options.dateFormat` | `string` | `MMM DD` | Moment date format |
|
|
358
|
+
| `options.timeFormat` | `string` | `h:mm A` | Moment time format |
|
|
359
|
+
| `options.extraEvents` | `show \| hide` | `hide` | Show events that do not match a milestone in tracking mode |
|
|
360
|
+
|
|
361
|
+
**EventSource:** `query.command`, `query.variables`, `query.path`, and `query.mapping` live under the `query` object. Mapping supports `id`, `key`, `date`, `title`, `description`, colors, icons, and additional fields. Tracking sources should map `key`. The component does not filter mapped events by `includeInTracking` or `isInactive`; apply those filters in the GraphQL query or source mapping when needed.
|
|
362
|
+
|
|
363
|
+
**Milestone:** `{ key, label?, description?, icon? }`. `label` can be localized (`{ en-US: "Delivered" }`).
|
|
299
364
|
|
|
300
365
|
**Events:** `onEventClick` (data: `event`)
|
|
301
366
|
|
|
@@ -307,12 +372,17 @@ props:
|
|
|
307
372
|
view: month
|
|
308
373
|
options:
|
|
309
374
|
enableNavigation: true
|
|
310
|
-
|
|
375
|
+
alternateSides: true
|
|
376
|
+
showTwoColumns: true
|
|
311
377
|
eventSources:
|
|
312
378
|
- query:
|
|
313
|
-
command: "query($id: Int!) { orderHistory(orderId: $id) { id title date type } }"
|
|
379
|
+
command: "query($id: Int!) { orderHistory(orderId: $id) { id title date type includeInTracking isInactive } }"
|
|
314
380
|
variables: { id: "{{ number orderId }}" }
|
|
315
|
-
path: orderHistory
|
|
381
|
+
path: data.orderHistory
|
|
382
|
+
mapping:
|
|
383
|
+
id: "{{ item.id }}"
|
|
384
|
+
date: "{{ item.date }}"
|
|
385
|
+
title: "{{ item.title }}"
|
|
316
386
|
eventTemplate:
|
|
317
387
|
component: card
|
|
318
388
|
name: eventCard
|
|
@@ -329,8 +399,78 @@ props:
|
|
|
329
399
|
props: { eventId: "{{ event.id }}" }
|
|
330
400
|
```
|
|
331
401
|
|
|
402
|
+
```yaml
|
|
403
|
+
component: timeline
|
|
404
|
+
name: orderTracking
|
|
405
|
+
props:
|
|
406
|
+
mode: tracking
|
|
407
|
+
orientation: vertical
|
|
408
|
+
options:
|
|
409
|
+
showTwoColumns: true
|
|
410
|
+
alternateSides: false
|
|
411
|
+
extraEvents: hide
|
|
412
|
+
eventSources:
|
|
413
|
+
- query:
|
|
414
|
+
command: >
|
|
415
|
+
query($organizationId: Int!, $orderId: Int!) {
|
|
416
|
+
order(organizationId: $organizationId, orderId: $orderId) {
|
|
417
|
+
trackingEvents {
|
|
418
|
+
trackingEventId
|
|
419
|
+
eventDate
|
|
420
|
+
description
|
|
421
|
+
includeInTracking
|
|
422
|
+
isInactive
|
|
423
|
+
eventDefinition { eventName }
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
variables:
|
|
428
|
+
organizationId: "{{ number organizationId }}"
|
|
429
|
+
orderId: "{{ number orderId }}"
|
|
430
|
+
path: order.trackingEvents
|
|
431
|
+
mapping:
|
|
432
|
+
key: "{{ item.eventDefinition.eventName }}"
|
|
433
|
+
id: "{{ item.trackingEventId }}"
|
|
434
|
+
date: "{{ item.eventDate }}"
|
|
435
|
+
title: "{{ item.eventDefinition.eventName }}"
|
|
436
|
+
description: "{{ item.description }}"
|
|
437
|
+
milestones:
|
|
438
|
+
- key: Shipment Created
|
|
439
|
+
label: { en-US: Shipment Created }
|
|
440
|
+
- key: In Transit
|
|
441
|
+
label: { en-US: In Transit }
|
|
442
|
+
icon: truck
|
|
443
|
+
- key: Delivered
|
|
444
|
+
label: { en-US: Delivered }
|
|
445
|
+
icon: check-circle
|
|
446
|
+
```
|
|
447
|
+
|
|
332
448
|
---
|
|
333
449
|
|
|
450
|
+
## planner
|
|
451
|
+
|
|
452
|
+
Use `planner` when columns are ordinal positions within grouped resource rows, not time buckets. Items flow through extraction (`queries[].itemsPath`), grouping (`rows.groupBy`), stable sorting (`columns.orderBy`), and index-based placement.
|
|
453
|
+
|
|
454
|
+
```yaml
|
|
455
|
+
component: planner
|
|
456
|
+
name: dispatchPlanner
|
|
457
|
+
props:
|
|
458
|
+
queries:
|
|
459
|
+
- name: work
|
|
460
|
+
query: { command: 'query Work { work { items } }', path: work.items }
|
|
461
|
+
- name: resources
|
|
462
|
+
query: { command: 'query Resources { resources { items } }', path: resources.items }
|
|
463
|
+
rows: { groupBy: resourceId, fromQuery: resources, key: id, headerWidth: 240 }
|
|
464
|
+
columns: { orderBy: [{ field: sequence, direction: asc }], appendEmpty: true, label: 'Move {{ index }}', width: 400 }
|
|
465
|
+
itemTemplate: { component: card }
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Optional templates cover row headers, empty cells/state, and summaries. Events are `onItemClick`, `onCellClick`, `onRowClick`, and `onItemsLoaded`. Explicit row-header and column widths are required for alignment when rows virtualize.
|
|
469
|
+
|
|
470
|
+
`dotsMenu.items` adds dataGrid-style card actions. Items support `label`, `permission`, templated `disabled`, and `onClick`; actions receive the full item context without also triggering `onItemClick`.
|
|
471
|
+
|
|
472
|
+
Set `columns.appendEmpty: true` to render one trailing empty ordinal slot beyond the largest group, ensuring every lane has an append target.
|
|
473
|
+
|
|
334
474
|
## timeline-grid
|
|
335
475
|
|
|
336
476
|
CSS Grid-based timeline with swim lanes, drill-down, and virtual scrolling.
|
|
@@ -358,7 +498,7 @@ CSS Grid-based timeline with swim lanes, drill-down, and virtual scrolling.
|
|
|
358
498
|
| Variable | Type | Description |
|
|
359
499
|
|----------|------|-------------|
|
|
360
500
|
| `dataSources` | `Record<string, TimelineEvent[]>` | Per-source events filtered to the column, keyed by `query.name` |
|
|
361
|
-
| `column` | `ColumnDefinition` | Column metadata (`id`, `label`, `date`, `startDate`, `endDate`) |
|
|
501
|
+
| `column` | `ColumnDefinition` | Column metadata (`id`, `label`, `date`, `startDate`, `endDate`, `startDateLocal`, `endDateLocal`) |
|
|
362
502
|
| `columnIndex` | `number` | Zero-based column index |
|
|
363
503
|
| `totalCount` | `number` | Total event count for the column across all sources |
|
|
364
504
|
|
|
@@ -366,7 +506,7 @@ CSS Grid-based timeline with swim lanes, drill-down, and virtual scrolling.
|
|
|
366
506
|
| Event | Data | Description |
|
|
367
507
|
|-------|------|-------------|
|
|
368
508
|
| `onEventClick` | `item, view` | Event clicked |
|
|
369
|
-
| `onCellClick` | `column, row, date, view` |
|
|
509
|
+
| `onCellClick` | `column, row, date, view` | Cell clicked; `column.startDate`/`endDate` are UTC ISO strings and `column.startDateLocal`/`endDateLocal` are local wall-clock `YYYY-MM-DDTHH:mm:ss` strings |
|
|
370
510
|
| `onViewChange` | `previousView, newView, startDate, endDate` | View changed |
|
|
371
511
|
| `onNavigate` | `direction, view, startDate, endDate` | Navigation |
|
|
372
512
|
| `onEventsLoaded` | `events, eventCount, view, dataRange` | Data loaded |
|
|
@@ -374,6 +514,8 @@ CSS Grid-based timeline with swim lanes, drill-down, and virtual scrolling.
|
|
|
374
514
|
|
|
375
515
|
**Column drill-down:** Click column header: year->month, month->week, week->day.
|
|
376
516
|
|
|
517
|
+
**Cell click date fields:** In `onCellClick`, prefer `column.startDateLocal` / `column.endDateLocal` when passing the clicked calendar range to local-day queries or dialogs. `column.startDate` / `column.endDate` remain UTC ISO instants for absolute timestamp use cases.
|
|
518
|
+
|
|
377
519
|
```yaml
|
|
378
520
|
component: timeline-grid
|
|
379
521
|
name: scheduleGrid
|
|
@@ -453,9 +595,14 @@ OAuth2 authorization flow button. Opens popup for auth, exchanges code for token
|
|
|
453
595
|
| `scopes` | `string[]` | Requested scopes |
|
|
454
596
|
| `additionalParams` | `Record<string, string>` | Extra auth URL params |
|
|
455
597
|
| `additionalHeaders` | `object` | Extra token request headers |
|
|
598
|
+
| `oauthWorkflowId` | `string` | Optional workflow ID for server-side token exchange through `executeWorkflow` |
|
|
456
599
|
| `label` | `ILocalizeString` | Button label (default: `Authorize`) |
|
|
457
600
|
| `className` | `string` | Button CSS class |
|
|
458
601
|
|
|
602
|
+
**Callback route:** configure OAuth providers with `/oauth2/callback`. The app redirects that stable URL to `/{locale}/oauth2/callback` and preserves the query string.
|
|
603
|
+
|
|
604
|
+
**Workflow exchange:** when `oauthWorkflowId` is set, the callback passes `organizationId` inside `ExecuteWorkflowInput` together with `credentials`, `tokenUrl`, and `headers`. The `organizationId` is saved before opening the popup because the callback popup starts with a fresh Redux store.
|
|
605
|
+
|
|
459
606
|
**Events:** `onToken` — fires with `{ token }` when OAuth completes.
|
|
460
607
|
|
|
461
608
|
```yaml
|
|
@@ -467,6 +614,7 @@ props:
|
|
|
467
614
|
clientSecret: "{{ quickbooksClientSecret }}"
|
|
468
615
|
authorizationUrl: "https://appcenter.intuit.com/connect/oauth2"
|
|
469
616
|
tokenUrl: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer"
|
|
617
|
+
oauthWorkflowId: "workflow-uuid-for-token-exchange" # optional
|
|
470
618
|
scopes:
|
|
471
619
|
- com.intuit.quickbooks.accounting
|
|
472
620
|
onToken:
|
|
@@ -68,7 +68,7 @@ npx cxtms create workflow <name> --template <template>
|
|
|
68
68
|
|
|
69
69
|
**`webhook`** — endpoint: `POST /api/v2/orgs/{organizationId}/webhooks/{workflowId}`. The endpoint is anonymous (`[AllowAnonymous]`) and rate-limited (10/sec, 100/min per IP). Two inputs are auto-injected by the controller: `payload` (parsed JSON body or raw string) and `request` (object with `headers`, `body`, `remoteIpAddress`). Control the HTTP response via `response` and `statusCode` outputs. Update `webhookSecret` configName to your app config path. Customize the `ValidateWebhook` step for your auth method (header secret, HMAC signature, etc.). Use `executionMode: Sync` when the caller needs a response; use `Async` for fire-and-forget (returns immediately). Keep `runAs: "system"` since the endpoint is anonymous. Add `additionalProperties.cors.allowedOrigins` to restrict CORS if needed.
|
|
70
70
|
|
|
71
|
-
**`public-api`** — requires a top-level `api` section defining the REST endpoint. Set `api.path` with route params (e.g., `/orders/{orderId}`), `api.method` (GET/POST/PUT/PATCH/DELETE), `api.authentication` (`none`, `bearer`, `apiKey`), `api.document` (swagger doc name, default `"public"`), and `api.category` (swagger tag). Configure `api.rateLimit` with `perSecond`/`perMinute`. Each input uses `props.in` (`path`, `query`, `header`, `body`) to specify where the parameter comes from, and `props.format` for OpenAPI type hints (e.g., `uuid`, `date-time`).
|
|
71
|
+
**`public-api`** — requires a top-level `api` section defining the REST endpoint. Set `api.path` with route params (e.g., `/orders/{orderId}`), `api.method` (GET/POST/PUT/PATCH/DELETE), `api.authentication` (`none`, `bearer`, `apiKey`), `api.document` (swagger doc name, default `"public"`), and `api.category` (swagger tag). Configure `api.rateLimit` with `perSecond`/`perMinute`. Each input uses `props.in` (`path`, `query`, `header`, `body`) to specify where the parameter comes from, and `props.format` for OpenAPI type hints (e.g., `uuid`, `date-time`). Body inputs may define `props.schema` with recursive OpenAPI schema fields (`type`, `properties`, `required`, `items`, `format`, `description`, `enum`); body inputs without a schema render as an object request body. Prefer `api.responses` for response documentation. Legacy `response` outputs can still use `props.type`, `props.description`, and recursive `props.schema` to describe the single `200` response. Must use `executionMode: Sync`. Control HTTP response via `response` and `statusCode` outputs.
|
|
72
72
|
|
|
73
73
|
**All templates** include workflow-level `events` (`onWorkflowStarted`, `onWorkflowCompleted`, `onWorkflowFailed`) and activity-level `events` (`onActivityStarted`, `onActivityCompleted`, `onActivityFailed`) with Log steps. Replace/extend these with notification tasks (Email/Send, HttpRequest, Workflow/Execute) as needed. (`onWorkflowExecuted` is a deprecated alias for `onWorkflowCompleted` — use `onWorkflowCompleted` in new workflows.)
|
|
74
74
|
|
|
@@ -191,10 +191,25 @@ events: # Workflow-level event handlers
|
|
|
191
191
|
For template expressions and value directives: see [ref-expressions-template.md](skills/cxtms-workflow-builder/ref-expressions-template.md)
|
|
192
192
|
For NCalc conditions and functions: see [ref-expressions-ncalc.md](skills/cxtms-workflow-builder/ref-expressions-ncalc.md)
|
|
193
193
|
|
|
194
|
-
**`{{ path }}`** — in step inputs. Single `{{ }}` returns raw object. Multiple returns string interpolation.
|
|
195
|
-
**`[variable]`** — in conditions and `expression:` directives. NCalc syntax.
|
|
196
|
-
**Value directives**: `expression`, `coalesce`, `foreach`, `switch`, `extends`, `$raw`
|
|
197
|
-
**
|
|
194
|
+
**`{{ path }}`** — in step inputs. Single `{{ }}` returns raw object. Multiple returns string interpolation. Supports sub-expressions `(variableName)` inside filter brackets for dynamic matching.
|
|
195
|
+
**`[variable]`** — in conditions and `expression:` directives. NCalc syntax. **Warning**: empty strings are silently converted to `null` — always use `isNullOrEmpty()` instead of comparing to `''`. See [ref-expressions-ncalc.md](skills/cxtms-workflow-builder/ref-expressions-ncalc.md) Pitfalls section.
|
|
196
|
+
**Value directives**: `expression`, `coalesce`, `foreach`, `switch`, `extends`, `$raw`, `$eval`, `resolve`, `decrypt`/`encrypt`
|
|
197
|
+
**Type converters** (prefix in `{{ }}`): `int`, `decimal`, `bool`, `boolOrFalse`, `boolOrTrue`, `datetime`, `string`, `emptyIfNull`, `nullIfEmpty`, `luceneString`, `transliterate`, `transliterateUa`, `fromJson`, `toJson`, `trim`, `toLocalTime`
|
|
198
|
+
**51 custom functions** + NCalc built-ins. Key ones: `isNullOrEmpty()`, `any()`, `all()`, `count()`, `sum()`, `first()`, `last()`, `contains()`, `join()`, `split()`, `format()`, `now()`, `addDays()`, `addHours()`, `dateDiff()`, `formatDate()`, `if()`, `groupBy()`, `concat()`, `distinct()`, `select()`, `zip()`, `regex()`, `fromJson()`
|
|
199
|
+
|
|
200
|
+
### Resolution Pipeline
|
|
201
|
+
|
|
202
|
+
Variables resolve through a 6-stage pipeline:
|
|
203
|
+
1. **Declaration** — `variables:` section sets static/computed/expression values
|
|
204
|
+
2. **Step input resolution** — `inputs:` values resolved per step
|
|
205
|
+
3. **Template resolution** — `{{ }}` expressions parsed by `TemplatedVariableParser` (type converters, sub-expressions, property paths)
|
|
206
|
+
4. **NCalc evaluation** — `conditions` and `expression:` directives evaluated by `ExpressionEvaluatorNCalc`
|
|
207
|
+
5. **foreach scoping** — `item` and `index` variables injected per iteration
|
|
208
|
+
6. **Output mapping** — step results stored as `ActivityName.StepName.outputKey`
|
|
209
|
+
|
|
210
|
+
### Runtime Boundary
|
|
211
|
+
|
|
212
|
+
Workflow `{{ }}` (server-side `TemplatedVariableParser`) and app module `{{ }}` (client-side custom template engine) are **different runtimes** with different syntax and capabilities. Workflow expressions support type converters, value directives, and NCalc conditions. App module expressions support their own operations: `eval`, `format`, `formatTz`, `hasPermission`, `isNullOrEmpty`, `any`, `isEqual`/`isEquals`, `moreThan`, `lessThan`, `dateDiff`, `daysBetween`, `daysUntil`, `daysAgo`, `isDateBefore`, `isDateAfter`, `round`, `trim`, `parse`, `startsWith`, `endsWith`, `includes`/`contains`, `isTrue`, `localStorage`, `sessionStorage`, `fromConfig`, `encodeURIComponent`.
|
|
198
213
|
|
|
199
214
|
### Null-Safe Operator `?` — USE BY DEFAULT
|
|
200
215
|
|
|
@@ -214,6 +229,30 @@ collection: "Activity?.Step?.output?.items?"
|
|
|
214
229
|
url: "{{ config?.baseUrl? }}"
|
|
215
230
|
```
|
|
216
231
|
|
|
232
|
+
### `fromConfig` Whole-Config Variables Include `organizationConfigId`
|
|
233
|
+
|
|
234
|
+
When a workflow-level variable loads an organization config without a `key`, the runtime injects `organizationConfigId` into the returned object alongside the config custom values. Use this ID when a later `OrganizationConfig/Update@1` step needs to update the same record.
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
variables:
|
|
238
|
+
- name: config
|
|
239
|
+
fromConfig:
|
|
240
|
+
configName: "apps.myApp"
|
|
241
|
+
|
|
242
|
+
activities:
|
|
243
|
+
- name: UpdateConfig
|
|
244
|
+
steps:
|
|
245
|
+
- task: "OrganizationConfig/Update@1"
|
|
246
|
+
inputs:
|
|
247
|
+
organizationId: "{{ int organizationId }}"
|
|
248
|
+
organizationConfigId: "{{ int config.organizationConfigId }}"
|
|
249
|
+
organizationConfig:
|
|
250
|
+
baseUrl: "{{ config?.baseUrl? }}"
|
|
251
|
+
lastSyncAt: "{{ datetime now }}"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
If `fromConfig.key` is provided, the variable resolves to only that keyed value and does not include `organizationConfigId`.
|
|
255
|
+
|
|
217
256
|
**Engine-level null safety for object inputs**: Task inputs resolved as complex objects (e.g., `headers`, `columnMappings`, configuration objects) are automatically null-safe at the engine level. If an optional object input is omitted from YAML, the engine returns `null` instead of throwing — no `?` suffix needed on the YAML key itself. The `?` operator is still required in template expressions and NCalc paths that reference those objects.
|
|
218
257
|
|
|
219
258
|
**When `?` is NOT needed** (guaranteed system variables):
|
|
@@ -386,7 +425,7 @@ npx cxtms workflow execute workflow.yaml --vars '{"mode": "preview"}' --file imp
|
|
|
386
425
|
|
|
387
426
|
`--file varName=path` uploads the local file to the server via presigned URL and sets the resulting URL as the named variable. Can be specified multiple times.
|
|
388
427
|
|
|
389
|
-
Returns execution result including `executionId`, `isAsync`, `outputs` (for Sync workflows).
|
|
428
|
+
Returns execution result including `executionId`, `isAsync`, `outputs` (for Sync workflows). When retry safety matters, pass a stable `executionId` per business operation so duplicate `executeWorkflow` attempts replay the stored result instead of running side effects again.
|
|
390
429
|
|
|
391
430
|
### Execution Logs
|
|
392
431
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- Order sub-entity tasks (OrderCommodity, OrderCharge, OrderDocument, OrderTrackingEvent, OrderEntity)
|
|
15
15
|
- Inventory tasks (InventoryItem Create, Update, Delete)
|
|
16
16
|
- Other entity tasks (Movement, Country, Cities, Rate, TrackingEvent/Import)
|
|
17
|
+
- Dispatch route generation tasks
|
|
17
18
|
- Notification tasks (Create)
|
|
18
19
|
- Note tasks (Create, Update, Delete, Import, Export, RenameThread)
|
|
19
20
|
- AccountingTransaction/ApplyCredit task
|
|
@@ -46,6 +47,39 @@ Imports postal codes (CSV, JSON, Excel). Supports upsert via `matchByFields`.
|
|
|
46
47
|
**Outputs:** `result.success`, `result.added`, `result.updated`, `result.errors`, `result.totalProcessed`, `result.hasErrors`
|
|
47
48
|
Input priority: `stream` > `fileUrl` > `postalCodes`. Task catches exceptions and returns them in `result.errors`.
|
|
48
49
|
|
|
50
|
+
## Dispatch Routes
|
|
51
|
+
|
|
52
|
+
| Task | Description |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `DispatchRoutes/Generate@1` | Generate daily draft dispatch routes from enabled templates |
|
|
55
|
+
| `OrderMove/Create@1` | Create an order move with nested legs |
|
|
56
|
+
| `OrderMove/Update@1` | Sparse-update a move and reconcile nested legs |
|
|
57
|
+
| `OrderMove/Delete@1` | Soft-delete a move and its legs |
|
|
58
|
+
|
|
59
|
+
### DispatchRoutes/Generate@1
|
|
60
|
+
|
|
61
|
+
Creates draft dispatch routes for enabled templates whose `daysOfWeek` contains the requested route date. Idempotent per template/date: existing generated routes are skipped.
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
- task: "DispatchRoutes/Generate@1"
|
|
65
|
+
name: GenerateDispatchRoutes
|
|
66
|
+
inputs:
|
|
67
|
+
organizationId: "{{ inputs.organizationId }}"
|
|
68
|
+
dispatchRouteStatusId: "{{ inputs.dispatchRouteStatusId }}"
|
|
69
|
+
routeDate: "{{ inputs.routeDate }}"
|
|
70
|
+
outputs:
|
|
71
|
+
- name: createdCount
|
|
72
|
+
mapping: "createdCount"
|
|
73
|
+
- name: skippedCount
|
|
74
|
+
mapping: "skippedCount"
|
|
75
|
+
- name: createdRouteIds
|
|
76
|
+
mapping: "createdRouteIds"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Inputs:** `organizationId` (string, required), `dispatchRouteStatusId` (string, required), `routeDate` (string?, `yyyy-MM-dd`, defaults to today UTC), `dispatchRouteTemplateId` (string?, generate one template only).
|
|
80
|
+
|
|
81
|
+
**Outputs:** `createdCount`, `skippedCount`, `createdRouteIds`. Generated routes copy template name, route type, division, and stops; driver/equipment assignment happens after generation.
|
|
82
|
+
|
|
49
83
|
## Generic Entity Change
|
|
50
84
|
|
|
51
85
|
| Task | Description |
|
|
@@ -88,11 +122,18 @@ Input priority: `stream` > `fileUrl` > `postalCodes`. Task catches exceptions an
|
|
|
88
122
|
entity:
|
|
89
123
|
customer: "{{ inputs.customerId }}"
|
|
90
124
|
status: "Draft"
|
|
125
|
+
values:
|
|
126
|
+
commodities:
|
|
127
|
+
- description: "{{ inputs.description }}"
|
|
128
|
+
pieces: "{{ inputs.pieces }}"
|
|
129
|
+
billToContactId: "{{ inputs.customerId }}"
|
|
91
130
|
outputs:
|
|
92
131
|
- name: order
|
|
93
132
|
mapping: "order"
|
|
94
133
|
```
|
|
95
134
|
|
|
135
|
+
For `Order/Create@1`, nested `values.commodities[*].billToContactId` is supported and is applied to the created commodity. Use it for multi-customer consolidations so downstream charge filtering can match commodity freight to the correct bill-to contact.
|
|
136
|
+
|
|
96
137
|
```yaml
|
|
97
138
|
- task: "Order/Update@2"
|
|
98
139
|
name: UpdateOrder
|
|
@@ -115,6 +156,7 @@ Imports order data from an external feed. Supports create and upsert (match-by-f
|
|
|
115
156
|
options:
|
|
116
157
|
orderMatchByFields: ["customValues.externalId"]
|
|
117
158
|
commodityMatchByFields: ["customValues.lineId"]
|
|
159
|
+
commodityMatchConflict: Error
|
|
118
160
|
skipValues: "NullOrEmpty" # default — preserves existing data when feed stops sending a field
|
|
119
161
|
outputs:
|
|
120
162
|
- name: result
|
|
@@ -131,6 +173,7 @@ Imports order data from an external feed. Supports create and upsert (match-by-f
|
|
|
131
173
|
| `inventoryItemMatchByFields` | string[] | null | Fields to match existing inventory items on commodities. |
|
|
132
174
|
| `tagMatchByFields` | string[] | null | Fields to match existing tags. |
|
|
133
175
|
| `commodityMatchByFields` | string[] | null | Fields to match existing commodities on update. |
|
|
176
|
+
| `commodityMatchConflict` | `Link` \| `Error` | `Link` | Handling when a matched top-level commodity belongs to another order of the same type. `Error` skips it and adds an import error. |
|
|
134
177
|
| `linkTrackingEventsToCommodities` | boolean | false | Link imported tracking events to first-level commodities. |
|
|
135
178
|
| `skipValues` | `None` \| `Null` \| `NullOrEmpty` | `NullOrEmpty` | Strip null/empty values before updating existing records. `NullOrEmpty` (default) prevents feeds that stop returning a field from wiping existing data (e.g. ETA after delivery). Use `None` to allow explicit null overwrites. |
|
|
136
179
|
|
|
@@ -141,6 +184,8 @@ Imports order data from an external feed. Supports create and upsert (match-by-f
|
|
|
141
184
|
|
|
142
185
|
**Order/Import commodity fields**: When importing commodities, you can supply `packageTypeName` (string) instead of `packageTypeId`. The import handler resolves the name to an ID using an N+1-safe per-import cache (one DB query per unique package type name).
|
|
143
186
|
|
|
187
|
+
Commodity matches are shared across the entire import session. Nested container commodities match positionally within their parent, preserving repeated same-key lines without duplicating them on re-import.
|
|
188
|
+
|
|
144
189
|
## Contact
|
|
145
190
|
|
|
146
191
|
| Task | Description |
|
|
@@ -149,6 +194,30 @@ Imports order data from an external feed. Supports create and upsert (match-by-f
|
|
|
149
194
|
| `Contact/Update` | Update contact |
|
|
150
195
|
| `Contact/Delete` | Delete contact |
|
|
151
196
|
|
|
197
|
+
**Contact import behavior:** backend contact imports validate `DivisionId` against the target organization. Invalid division IDs are ignored; new contacts default to the importing user's division when no valid division is provided. Nested `division.divisionName` can be supplied and is resolved to an organization division before import.
|
|
198
|
+
|
|
199
|
+
## OrganizationConfig
|
|
200
|
+
|
|
201
|
+
| Task | Description |
|
|
202
|
+
|------|-------------|
|
|
203
|
+
| `OrganizationConfig/Update@1` | Update an existing organization configuration record with dynamic values |
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
- task: "OrganizationConfig/Update@1"
|
|
207
|
+
name: UpdateOrgConfig
|
|
208
|
+
inputs:
|
|
209
|
+
organizationId: "{{ int inputs.organizationId }}"
|
|
210
|
+
organizationConfigId: "{{ int vars.config.organizationConfigId }}"
|
|
211
|
+
organizationConfig:
|
|
212
|
+
values:
|
|
213
|
+
enabled: true
|
|
214
|
+
outputs:
|
|
215
|
+
- name: result
|
|
216
|
+
mapping: "result?"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Inputs:** `organizationId` (int, required), `organizationConfigId` (int, required), `organizationConfig` (object, required dynamic values). Returns an empty object on success. Query by config name first if you do not already have the ID.
|
|
220
|
+
|
|
152
221
|
## Contact Address
|
|
153
222
|
|
|
154
223
|
| Task | Description |
|
|
@@ -156,7 +225,32 @@ Imports order data from an external feed. Supports create and upsert (match-by-f
|
|
|
156
225
|
| `ContactAddress/Create` | Create address |
|
|
157
226
|
| `ContactAddress/Update` | Update address |
|
|
158
227
|
| `ContactAddress/Delete` | Delete address |
|
|
159
|
-
| `ContactAddress/Import` | Bulk import addresses |
|
|
228
|
+
| `ContactAddress/Import` | Bulk import addresses; ID-first upsert when `ContactAddressId` or `contactAddressId` is present |
|
|
229
|
+
|
|
230
|
+
**Selective import fetch:** `ContactAddress/Import@1` matches rows by `ContactAddressId`/`contactAddressId` first, then by `matchByFields`. Rows are bound as dictionaries, so primary-key columns and custom inbound column names survive even when there is no DTO property. When `matchByFields` is set, the importer builds batched Lucene filters from the incoming row values and fetches only candidate `AddressType.Other` addresses for the target contact. If `matchByFields` is omitted, it falls back to loading all `Other` addresses for that contact. Use stable fields such as `AddressLine`, `CityName`, `CountryCode`, `PostalCode`, and `StateCode` for large imports.
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
### ContactAddress/Import@1
|
|
234
|
+
|
|
235
|
+
Bulk imports contact addresses for one `organizationId` + `contactId` from file/stream/import data. The import reads rows as dictionaries, so column mappings and aliases are supported.
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
- task: "ContactAddress/Import@1"
|
|
239
|
+
name: ImportContactAddresses
|
|
240
|
+
inputs:
|
|
241
|
+
organizationId: "{{ int inputs.organizationId }}"
|
|
242
|
+
contactId: "{{ int inputs.contactId }}"
|
|
243
|
+
fileUrl: "{{ inputs.fileUrl }}"
|
|
244
|
+
fileType: "Csv"
|
|
245
|
+
matchByFields: [AddressLine, AddressLine2, CityName, CountryCode, PostalCode, StateCode]
|
|
246
|
+
columnMappings:
|
|
247
|
+
City: CityName
|
|
248
|
+
outputs:
|
|
249
|
+
- name: result
|
|
250
|
+
mapping: "result?"
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Behavior:** rows with `ContactAddressId` or `contactAddressId` match existing addresses by ID first; otherwise `matchByFields` is used. New rows default to `AddressType.Other`. `City` aliases to `CityName`; `StateCode` is used directly or `StateName` is resolved. `Longitude` + `Latitude` set location. Rows missing country or state context are skipped.
|
|
160
254
|
|
|
161
255
|
## Contact Payment Method
|
|
162
256
|
|
|
@@ -172,7 +266,7 @@ Imports order data from an external feed. Supports create and upsert (match-by-f
|
|
|
172
266
|
| Task | Description |
|
|
173
267
|
|------|-------------|
|
|
174
268
|
| `Commodity/Create` | Create commodity |
|
|
175
|
-
| `Commodity/Update` (v1, v2) | Update commodity |
|
|
269
|
+
| `Commodity/Update` (v1, v2) | Update commodity; supports scan custom-value cascade flags `cascadeToChildren` and `promoteParentWhenAllChildren` |
|
|
176
270
|
| `Commodity/Split` | Split commodity into multiple |
|
|
177
271
|
| `Commodity/Repack` | Repack commodities |
|
|
178
272
|
| `Commodity/Unpack` | Unpack commodity |
|
|
@@ -298,8 +392,8 @@ Imports order data from an external feed. Supports create and upsert (match-by-f
|
|
|
298
392
|
| `Country/Create`, `Country/Update`, `Country/Delete` | Country CRUD |
|
|
299
393
|
| `Cities/Import` | Import cities |
|
|
300
394
|
| `Rate/Update` | Update rate |
|
|
301
|
-
| `TrackingEvent/Create` | Create a single tracking event and link to an order,
|
|
302
|
-
| `TrackingEvent/Import` | Batch import tracking events into
|
|
395
|
+
| `TrackingEvent/Create` | Create a single tracking event and link to an order, commodity, dispatch route, or dispatch route stop |
|
|
396
|
+
| `TrackingEvent/Import` | Batch import tracking events into exactly one order, dispatch route, or dispatch route stop |
|
|
303
397
|
|
|
304
398
|
```yaml
|
|
305
399
|
# Create a single tracking event linked to a commodity (or a list of commodities)
|
|
@@ -319,6 +413,8 @@ Imports order data from an external feed. Supports create and upsert (match-by-f
|
|
|
319
413
|
|
|
320
414
|
Use [`OrderTrackingEvent/Create@1`](#order-sub-entities) when you want per-task overrides of the commodity auto-link behavior (`autoLinkToCommodities`, `commodityIds`). `TrackingEvent/Create@1` only honors the org-wide `tms.trackingEvents.autoLinkToCommodities` default for the auto-link step.
|
|
321
415
|
|
|
416
|
+
For dispatch milestones, pass `dispatchRouteId` or `dispatchRouteStopId` to `TrackingEvent/Create@1`. For `TrackingEvent/Import@1`, provide exactly one of `orderId`, `dispatchRouteId`, or `dispatchRouteStopId`; route and stop imports do not auto-link commodities.
|
|
417
|
+
|
|
322
418
|
```yaml
|
|
323
419
|
# Batch import tracking events
|
|
324
420
|
- task: "TrackingEvent/Import@1"
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
- Operators (comparison, logical, arithmetic, ternary, membership)
|
|
6
6
|
- Iterator variables (`[each.*]` and `[item.*]`)
|
|
7
7
|
- Collection functions (any, all, count, sum, first, last, distinct, select, zip, groupBy, join, sort, etc.)
|
|
8
|
-
- String functions (isNullOrEmpty, length, lower, upper, replace, format, base64, coalesce, etc.)
|
|
9
|
-
- Date functions (now, parseDate, addDays, formatDate, dateFromUnix,
|
|
8
|
+
- String functions (isNullOrEmpty, length, lower, upper, replace, format, base64, fromJson, regex, coalesce, etc.)
|
|
9
|
+
- Date functions (now, parseDate, addDays, addHours, dateDiff, formatDate, dateFromUnix, dateToUtc, toLocalTime)
|
|
10
10
|
- Math functions (Abs, Ceiling, Floor, Round, Min, Max, etc.)
|
|
11
11
|
- Domain functions (convertWeight, convertDimension)
|
|
12
|
+
- Variable resolution details
|
|
13
|
+
- Pitfalls (empty string null trap)
|
|
12
14
|
|
|
13
15
|
For template expressions `{{ path }}` used in step inputs, see [ref-expressions-template.md](ref-expressions-template.md).
|
|
14
16
|
|
|
@@ -85,6 +87,8 @@ Functions use two iterator variable names:
|
|
|
85
87
|
| `trim([value])` | Trim whitespace. Returns `""` for null |
|
|
86
88
|
| `format('{0}-{1}', [prefix], [id])` | String.Format style. Variadic args. Returns null if format is null |
|
|
87
89
|
| `base64([value])` / `fromBase64([encoded])` | Base64 encode/decode. Handles string, byte[], JToken |
|
|
90
|
+
| `fromJson([str])` | Parse JSON string to Dictionary or JArray. Returns `null` for null input |
|
|
91
|
+
| `regex([str], 'pattern')` | Regex match. Returns match result |
|
|
88
92
|
| `bool([value])` | Convert to boolean: null->`false`, empty string->`false`, "true"/"false"->parsed, non-zero number->`true`, any object->`true` |
|
|
89
93
|
| `transliterate([value])` | Unicode to ASCII (Unidecode). Returns `""` for null |
|
|
90
94
|
| `transliterateUa([value])` | Ukrainian-specific transliteration. Returns `""` for null |
|
|
@@ -101,6 +105,7 @@ Functions use two iterator variable names:
|
|
|
101
105
|
| `now('yyyy-MM-dd', 'en-US')` | Formatted current time as string |
|
|
102
106
|
| `addDays([date], 30)` | Add days (decimal, can be negative). Accepts DateTime, DateTimeOffset, string |
|
|
103
107
|
| `addHours([date], 2)` | Add hours (decimal, can be negative). Same type handling |
|
|
108
|
+
| `dateDiff([start], [end], 'unit')` | Difference between two dates in specified unit |
|
|
104
109
|
| `formatDate([date], 'dd/MM/yyyy')` | Format date using InvariantCulture (culture optional). Returns `null` for null or unparseable input. Accepts `DateTime`, `DateTimeOffset`, or string (including `DateTimeOffset`-shaped strings from `toLocalTime`) |
|
|
105
110
|
| `formatDate([date], 'dd/MM/yyyy', 'en-US')` | Format date with explicit culture |
|
|
106
111
|
| `dateFromUnix([unixTime])` | Unix timestamp (seconds) -> `DateTimeOffset`. Accepts int, long, decimal, string |
|
|
@@ -135,3 +140,31 @@ Custom: `ceiling([value])` -- same as `Ceiling` but handles type conversion to d
|
|
|
135
140
|
|----------|-------------|
|
|
136
141
|
| `convertWeight([weight], 'Kg', 'Lb')` | Weight unit conversion. Returns `decimal` rounded to 5 places |
|
|
137
142
|
| `convertDimension([length], 'Cm', 'In')` | Dimension unit conversion. Returns `decimal` rounded to 3 places |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Variable Resolution Details
|
|
147
|
+
|
|
148
|
+
- `[variableName]` → workflow variable dictionary lookup via `GetPropertyValue()`
|
|
149
|
+
- `[order.status]` → dot-path traversal through nested dictionaries/reflection
|
|
150
|
+
- Without `?` → throws `InvalidOperationException` on missing path
|
|
151
|
+
- With `?` → returns null instead of throwing
|
|
152
|
+
- Empty string → null conversion (see Pitfalls below)
|
|
153
|
+
- `FormatException` → retries with numeric string conversion (`"42"` → `42m`)
|
|
154
|
+
|
|
155
|
+
## Pitfalls
|
|
156
|
+
|
|
157
|
+
### Empty String Null Trap
|
|
158
|
+
|
|
159
|
+
The NCalc engine silently converts `""` (empty string) to `null` **before** evaluating expressions. This happens in `EvaluateParameter` — it uses `string.IsNullOrEmpty()` (not `IsNullOrWhiteSpace`), so whitespace-only strings like `" "` pass through as-is.
|
|
160
|
+
|
|
161
|
+
**Consequence**: `[myVar] != ''` is **always true** when the variable is empty, because the empty string becomes `null` and `null != ''` evaluates to `true`.
|
|
162
|
+
|
|
163
|
+
**Rule**: Never compare against empty string. Always use `isNullOrEmpty()` instead:
|
|
164
|
+
```yaml
|
|
165
|
+
# WRONG — always true when myVar is empty
|
|
166
|
+
expression: "[myVar] != ''"
|
|
167
|
+
|
|
168
|
+
# CORRECT
|
|
169
|
+
expression: "isNullOrEmpty([myVar]) = false"
|
|
170
|
+
```
|
|
@@ -24,6 +24,8 @@ inputs:
|
|
|
24
24
|
|
|
25
25
|
**Date string normalization in step inputs**: When a string value is merged into a step's input dictionary (via `AddRangeN`), the engine auto-detects date/datetime strings and converts them to ISO format. Common formats recognized: ISO (`2024-03-15`), US (`03/15/2024`), EU (`25/12/2024`), datetime with offset (`2024-03-15T15:30:45+05:00` → UTC). Empty strings are converted to `null`. OLE Automation date numbers (e.g., `"45752"`) are also recognized as dates — but only when the field name includes a date/time keyword (e.g., `departureDate`, `pickupTime`). A numeric string like `"45752"` for a field named `amount` is kept as-is.
|
|
26
26
|
|
|
27
|
+
**DateTime assignment in dynamic entity updates**: Values assigned to `DateTime` or nullable `DateTime` entity fields are normalized to UTC before persistence. Offset-bearing strings and local/offset values are shifted to the equivalent UTC instant. Zone-less strings and `DateTime` values with an unspecified kind are assumed UTC without changing their wall-clock value. Empty strings become `null` only for nullable fields; use ISO 8601 with `Z` or an explicit offset whenever the source represents a known instant.
|
|
28
|
+
|
|
27
29
|
### Type Converters (prefix in {{ }})
|
|
28
30
|
|
|
29
31
|
```yaml
|
|
@@ -180,9 +182,10 @@ Used in `collection:` (foreach), `mapping:` (outputs), and variable resolution.
|
|
|
180
182
|
| `list[*]` | Flatten/wildcard (all items) | `containers[*].commodities` |
|
|
181
183
|
| `list[*].dictKey` | Wildcard traversal into Dictionary/JObject keys | `items[*].customValues.chapter_en` |
|
|
182
184
|
| `list[**]` | Recursive flatten (all depths) | `containerCommodities[**]` |
|
|
183
|
-
| `list[-1]` |
|
|
185
|
+
| `list[-1]` | Tree-depth filter from end (meaningful after `[**]` recursive flatten) | `tree[**][-1]` |
|
|
184
186
|
| `list[condition]` | Filter by condition. LHS supports dotted nested paths | `items[status=Active]`, `activity[status.type=D]` |
|
|
185
187
|
| `dict['key']` | Dictionary key access | `customValues['myField']` |
|
|
188
|
+
| `dict[variableName]` | Dynamic key — resolves variableName as property path, uses result as dict key | `customValues[mode]` |
|
|
186
189
|
| `list[*].{f1 f2}` | Field selector (projection) | `items[*].{name description}` |
|
|
187
190
|
| `list[*].{alias:source}` | Field selector with alias | `items[*].{id:commodityId}` |
|
|
188
191
|
| `list[*].{alias:_.parent}` | Field selector referencing parent | `items[*].{parentId:_.orderId}` |
|
|
@@ -190,3 +193,14 @@ Used in `collection:` (foreach), `mapping:` (outputs), and variable resolution.
|
|
|
190
193
|
**Wildcard traversal into Dictionary/JObject**: After `[*]`, subsequent path segments drill into Dictionary keys and JObject properties on each item. Dictionary-like values are preserved intact (not flattened) so multi-hop paths work: `items[*].customValues.chapter_en` extracts the `chapter_en` key from each item's `customValues` dictionary. This also works with nested dictionaries (`items[*].meta.locale.name`) and JObject items from JSON payloads.
|
|
191
194
|
|
|
192
195
|
**JArray primitive unwrapping**: When `GetPropertyValue` encounters a JArray where every element is a JValue (primitive), it automatically unwraps the array into a `List<object>` of plain .NET values. This ensures downstream iteration (e.g., `select()`, `zip()`, `foreach`) works with primitives rather than JValue wrappers.
|
|
196
|
+
|
|
197
|
+
### Sub-expressions
|
|
198
|
+
|
|
199
|
+
Parenthesized `(variableName)` inside filter brackets resolves the inner variable first, then uses its value as the filter match target:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
# selected_status resolves to e.g. "Active", then items is filtered where status matches
|
|
203
|
+
filteredName: "{{ order.items.[status=(selected_status)].[0].name }}"
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The inner variable `(selected_status)` is resolved through the same property path engine before the filter expression is evaluated. This enables dynamic filtering where the match value comes from a workflow variable rather than a literal.
|