@cxtms/cx-schema 1.9.259 → 1.9.261

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 (34) hide show
  1. package/dist/cli.js +2 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/types.d.ts +1 -1
  4. package/dist/types.d.ts.map +1 -1
  5. package/dist/workflowValidator.d.ts +7 -0
  6. package/dist/workflowValidator.d.ts.map +1 -1
  7. package/dist/workflowValidator.js +54 -0
  8. package/dist/workflowValidator.js.map +1 -1
  9. package/package.json +1 -1
  10. package/schemas/actions/all.json +1 -0
  11. package/schemas/actions/printLabel.json +20 -0
  12. package/schemas/actions/sound.json +4 -3
  13. package/schemas/actions/vibrate.json +6 -4
  14. package/schemas/components/README.md +1 -0
  15. package/schemas/components/avatar.json +117 -1
  16. package/schemas/components/infoLine.json +82 -1
  17. package/schemas/components/planner.json +369 -1
  18. package/schemas/components/progressBar.json +115 -1
  19. package/schemas/fields/attachment.json +35 -2
  20. package/schemas/workflows/agent/agent.json +98 -0
  21. package/schemas/workflows/tasks/attachment.json +58 -2
  22. package/schemas/workflows/workflow.json +14 -19
  23. package/skills/cxtms-developer/ref-entity-notification.md +2 -2
  24. package/skills/cxtms-developer/ref-entity-shared.md +6 -2
  25. package/skills/cxtms-module-builder/SKILL.md +84 -5
  26. package/skills/cxtms-module-builder/ref-components-data.md +1 -1
  27. package/skills/cxtms-module-builder/ref-components-display.md +98 -15
  28. package/skills/cxtms-module-builder/ref-components-forms.md +27 -1
  29. package/skills/cxtms-module-builder/ref-components-interactive.md +3 -3
  30. package/skills/cxtms-module-builder/ref-components-layout.md +1 -1
  31. package/skills/cxtms-workflow-builder/SKILL.md +3 -1
  32. package/skills/cxtms-workflow-builder/ref-agent.md +283 -0
  33. package/skills/cxtms-workflow-builder/ref-communication.md +37 -10
  34. package/templates/workflow-agent.yaml +48 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "Attachment Tasks",
4
- "description": "Attachment entity CRUD operations",
4
+ "description": "Attachment entity CRUD operations and entity links",
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "task": {
@@ -9,6 +9,8 @@
9
9
  "enum": [
10
10
  "Attachment/Create@1",
11
11
  "Attachment/Update@1",
12
+ "Attachment/Link@1",
13
+ "Attachment/Unlink@1",
12
14
  "Attachment/Delete@1",
13
15
  "Attachment/Get@1",
14
16
  "Attachment/Thumbnail",
@@ -48,13 +50,67 @@
48
50
  "type": "string",
49
51
  "description": "Attachment ID"
50
52
  },
53
+ "attachment": {
54
+ "type": "object",
55
+ "description": "Attachment values (Attachment/Create@1, Attachment/Update@1)",
56
+ "properties": {
57
+ "fileName": {
58
+ "type": "string"
59
+ },
60
+ "attachmentType": {
61
+ "type": "string",
62
+ "description": "Picture, OtherDocument, Avatar, CustomerDocument"
63
+ },
64
+ "description": {
65
+ "type": "string"
66
+ },
67
+ "parentType": {
68
+ "type": "string",
69
+ "description": "Primary parent type: Order, Contact, AccountingTransaction, EquipmentType, Job, Commodity, Route, TrackingEvent"
70
+ },
71
+ "parentId": {
72
+ "type": "string",
73
+ "description": "Primary parent ID"
74
+ },
75
+ "links": {
76
+ "type": "array",
77
+ "description": "Additional entity links on create (Attachment/Create@1)",
78
+ "items": {
79
+ "type": "object",
80
+ "properties": {
81
+ "entityType": {
82
+ "type": "string",
83
+ "description": "Linked entity type: Order, Contact, Job, TrackingEvent"
84
+ },
85
+ "entityId": {
86
+ "type": "string",
87
+ "description": "Linked entity ID (int id; uuid for Job)"
88
+ }
89
+ },
90
+ "required": ["entityType", "entityId"]
91
+ }
92
+ }
93
+ },
94
+ "additionalProperties": true
95
+ },
96
+ "fileData": {
97
+ "description": "File content: base64 string, byte array or stream (Attachment/Create@1)"
98
+ },
99
+ "fileUrl": {
100
+ "type": "string",
101
+ "description": "URL to download the file from (Attachment/Create@1)"
102
+ },
51
103
  "entityName": {
52
104
  "type": "string",
53
105
  "description": "Parent entity name"
54
106
  },
107
+ "entityType": {
108
+ "type": "string",
109
+ "description": "Entity type to link/unlink: Order, Contact, Job, TrackingEvent (Attachment/Link@1, Attachment/Unlink@1)"
110
+ },
55
111
  "entityId": {
56
112
  "type": "string",
57
- "description": "Parent entity ID"
113
+ "description": "Entity ID to link/unlink: int id, uuid for Job (Attachment/Link@1, Attachment/Unlink@1)"
58
114
  },
59
115
  "fileName": {
60
116
  "type": "string",
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "workflowType": {
69
69
  "type": "string",
70
- "enum": ["Document", "Quote", "Flow", "Webhook", "PublicApi", "McpTool", "McpResource", "McpPrompt"],
71
- "description": "Workflow type, including organization-scoped MCP tools, resources, and prompts. Omit for standard process workflows."
70
+ "enum": ["Process", "Document", "Quote", "Flow", "Webhook", "PublicApi", "McpTool", "McpResource", "McpPrompt", "Agent", "EmailTemplate", "RulesTariff"],
71
+ "description": "Workflow type: Process (standard workflow), Document (PDF/Excel generation), Quote, Flow (declarative state machine), Webhook (HTTP endpoint), PublicApi (REST API endpoint), McpTool, McpResource, McpPrompt (organization-scoped MCP tools, resources and prompts), Agent (LLM agent), EmailTemplate or RulesTariff. Omit for standard process workflows."
72
72
  },
73
73
  "runAs": {
74
74
  "type": "string",
@@ -279,6 +279,7 @@
279
279
  },
280
280
  "additionalProperties": false
281
281
  },
282
+ "agent": { "$ref": "agent/agent.json" },
282
283
  "entity": {
283
284
  "$ref": "flow/entity.json",
284
285
  "description": "Entity configuration for Flow workflows"
@@ -308,24 +309,18 @@
308
309
  "required": ["workflow"],
309
310
  "allOf": [
310
311
  {
311
- "if": {
312
- "properties": {
313
- "workflow": {
314
- "properties": {
315
- "workflowType": { "const": "Flow" }
316
- },
317
- "required": ["workflowType"]
318
- }
319
- }
320
- },
321
- "then": {
322
- "required": ["workflow", "entity"],
323
- "properties": {
324
- "activities": false
325
- }
326
- },
312
+ "if": { "properties": { "workflow": { "properties": { "workflowType": { "const": "Flow" } }, "required": ["workflowType"] } } },
313
+ "then": { "required": ["workflow", "entity"], "properties": { "activities": false } },
327
314
  "else": {
328
- "required": ["workflow", "activities"]
315
+ "if": { "properties": { "workflow": { "properties": { "workflowType": { "const": "Agent" } }, "required": ["workflowType"] } } },
316
+ "then": {
317
+ "required": ["workflow", "agent"],
318
+ "properties": {
319
+ "activities": false,
320
+ "workflow": { "properties": { "executionMode": { "const": "Sync" } }, "required": ["executionMode"] }
321
+ }
322
+ },
323
+ "else": { "required": ["workflow", "activities"] }
329
324
  }
330
325
  },
331
326
  {
@@ -13,8 +13,8 @@ Real-time notification system. Notifications are org-scoped with per-user read t
13
13
  | `type` | `NotificationType` | System=0, OrderUpdate=1, TaskAssignment=2, Alert=3, Info=4 |
14
14
  | `priority` | `NotificationPriority` | Low=0, Normal=1, High=2, Urgent=3 |
15
15
  | `targetUserId` | `string?` | If set, targets one user; if null, broadcasts to all active org users |
16
- | `entityType` | `string?` | Linked entity type (e.g. "Order", "Job") |
17
- | `entityId` | `int?` | Linked entity PK |
16
+ | `entityType` | `string?` | Linked entity type (e.g. "Order", "Job", "AgentSession") |
17
+ | `entityId` | `string?` | Linked entity id (varchar(64)). Most entity types still store an integer PK as text; `entityType: "AgentSession"` carries the session's GUID (`AgentSessionId.ToString()`) instead. |
18
18
  | `expiresAt` | `DateTime?` | Optional expiration |
19
19
  | `created` / `lastModified` | `DateTime` | Audit fields (from `AuditableEntity`) |
20
20
  | `createdBy` / `lastModifiedBy` | `string` | Audit fields |
@@ -77,7 +77,7 @@ Tagging system for orders, commodities, inventory items.
77
77
 
78
78
  ## Attachment
79
79
 
80
- File attachments linked to orders, contacts, jobs, etc.
80
+ File attachments. `parentType`/`parentId` is the primary parent; an attachment can also be linked to more Orders, Contacts, Jobs and TrackingEvents (link tables).
81
81
 
82
82
  | Field | Type | Notes |
83
83
  |-------|------|-------|
@@ -91,7 +91,7 @@ File attachments linked to orders, contacts, jobs, etc.
91
91
  | `description` | `string?` | |
92
92
  | `attachmentType` | `AttachmentType` enum | Picture=1, OtherDocument, Avatar, CustomerDocument |
93
93
  | `parentId` | `string?` | Polymorphic FK (entity ID as string) |
94
- | `parentType` | `AttachmentParentType` enum | None=0, Order=1, Contact=2, AccountingTransaction=3, EquipmentType=4, Job=5, Commodity=6 |
94
+ | `parentType` | `AttachmentParentType` enum | None=0, Order=1, Contact=2, AccountingTransaction=3, EquipmentType=4, Job=5, Commodity=6, Route=7, TrackingEvent=8 |
95
95
  | `status` | `AttachmentStatus` enum | Active=0, PendingUpload=1, UploadFailed=2 |
96
96
  | `category` | `AttachmentCategory` enum | General=0, FieldValue=1 |
97
97
  | `organizationId` | `int` | |
@@ -103,6 +103,10 @@ File attachments linked to orders, contacts, jobs, etc.
103
103
  - `presignedFileUri`, `presignedPreviewUri`, `presignedThumbnailUri` — signed URLs
104
104
  - `getPresignedUri(expiresInDays, uriType)` — custom resolver
105
105
  - `getParentOrder` — resolve parent Order
106
+ - `links { entityType entityId }` — all links, including the primary one
107
+ - Filter by link: `attachments(filter: "orderLinks.orderId:123")` — also `contactLinks.contactId`, `jobLinks.jobId`, `trackingEventLinks.trackingEventId`
108
+ - `job { getJobAttachments }`, `trackingEvent { getTrackingEventAttachments }` — attachments linked to the entity
109
+ - Mutations: `linkAttachment` / `unlinkAttachment` (`attachmentId`, `entityType`, `entityId`); `createAttachment` `values.links: [{ entityType, entityId }]`. Workflows: `Attachment/Link@1`, `Attachment/Unlink@1`
106
110
 
107
111
  ---
108
112
 
@@ -305,6 +305,7 @@ onClick:
305
305
  onSuccess: [...]
306
306
  onError: [...]
307
307
  - fileDownload: { url: "...", fileName: "..." }
308
+ - printLabel: { link: "{{ labelUrl }}", type: "pdf" } # web: print template-resolved content with print-js
308
309
  - forEach:
309
310
  items: "{{ selectedItems }}"
310
311
  item: "currentItem"
@@ -315,8 +316,9 @@ onClick:
315
316
  - consoleLog: { message: "debug info" }
316
317
  - openBarcodeScanner: { onScan: [...] }
317
318
  - resetDirtyState: {}
318
- - sound: success # success|error|warning|scan
319
- - vibrate: light # success|error|warning|light|medium|heavy
319
+ - sound: success # audible cue: success|error|warning|scan (or { type, volume: 0-1 })
320
+ - vibrate: success # haptic: success|error|warning|light|medium|heavy
321
+ - vibrate: { pattern: [100, 50, 100] } # custom ms pattern (web Vibration API convention) or { duration: 200 }
320
322
  ```
321
323
 
322
324
  ## Common Patterns
@@ -334,10 +336,87 @@ value: "{{ fieldName }}" # Simple variable
334
336
  value: "{{ format date L }}" # Format helper
335
337
  value: "{{ number quantity }}" # Type cast
336
338
  value: "{{ sessionStorage isDevMode }}" # Current-tab sessionStorage
337
- value: "{{ eval items.length > 0 }}" # JavaScript expression
338
- isHidden: "{{ eval !canEdit }}" # Conditional visibility
339
+ isHidden: "{{ !canEdit }}" # Negation — no eval needed
340
+ value: "{{ eval items.map(x => x.id) }}" # JavaScript eval — last resort
339
341
  ```
340
342
 
343
+ **Prefer built-in template functions over `eval`.** The engine ships named functions for
344
+ the common cases — use them first. Reach for `eval` only when nothing below fits
345
+ (array `.map()`/`.filter()`, arithmetic, template literals, object building, ternaries).
346
+
347
+ | Function | Example | Notes |
348
+ |----------|---------|-------|
349
+ | `number` / `number!` | `{{ number quantity }}` | Cast to number; `number!` returns `0` for null |
350
+ | `string` / `boolean` | `{{ boolean isActive }}` | Cast; `boolean` treats string `'true'` as true |
351
+ | `nullIfEmpty` | `{{ nullIfEmpty notes }}` | `''`, `0`, undefined → null |
352
+ | `luceneString` | `{{ luceneString search }}` | Escape for Lucene filter strings |
353
+ | `isEqual` | `{{ isEqual status 'Pending' }}` | Deep equality; numeric strings coerced |
354
+ | `isTrue` | `{{ isTrue customValues.flag }}` | `true`, `'true'`, or truthy |
355
+ | `isNullOrEmpty` | `{{ isNullOrEmpty orderNumber }}` | null, undefined, `''`, or empty array |
356
+ | `any` | `{{ any selectedItems }}` | Array/string has at least one element |
357
+ | `moreThan` / `lessThan` | `{{ moreThan totalAmount 100 }}` | Numbers by value; arrays/strings by length |
358
+ | `startsWith` / `endsWith` | `{{ startsWith trackingNumber 'TRK' }}` | String prefix/suffix check |
359
+ | `includes` / `contains` | `{{ includes description 'rush' }}` | Substring check (aliases) |
360
+ | `trim` | `{{ trim name }}` | Trim whitespace |
361
+ | `round` | `{{ round totalAmount 2 }}` | `toFixed(n)` returned as number |
362
+ | `encodeURIComponent` | `{{ encodeURIComponent returnUrl }}` | URL-encode |
363
+ | `format` | `{{ format createdDate L }}` | moment format (`L`, `MM/DD/YYYY`, `fromNow`, …) |
364
+ | `formatTz` | `{{ formatTz pickupDate 'MM/DD h:mm A' 'America/New_York' }}` | Format UTC value in a timezone |
365
+ | `dateDiff` | `{{ dateDiff dueDate startDate }}` | Whole days, first minus second |
366
+ | `daysBetween` | `{{ daysBetween pickupDate deliveryDate }}` | Absolute day difference |
367
+ | `daysUntil` / `daysAgo` | `{{ daysUntil dueDate }}` | Days between now and the date |
368
+ | `isDateBefore` / `isDateAfter` | `{{ isDateBefore pickupDate deliveryDate }}` | Date comparison |
369
+ | `hasPermission` | `{{ hasPermission Orders/Update }}` | Current user permission check |
370
+ | `fromConfig` | `{{ fromConfig apps.myFeature key }}` | Organization config lookup |
371
+ | `localStorage` / `sessionStorage` | `{{ localStorage recentSearch }}` | Browser storage read |
372
+ | `parse` | `{{ parse configs.labelTemplate }}` | Re-parse a template stored in a variable |
373
+
374
+ Combinators — also no `eval` needed:
375
+ - `!` / `!!` prefix negates / booleanizes any path or function: `{{ !isEqual status 'Pending' }}`, `{{ !!order.notes }}`
376
+ - `||` / `&&` combine paths and comparisons: `{{ orderId || quoteId }}`, `{{ !pickupForm.contactAddressId || !pickupForm.pickupDate }}`
377
+ - `?.` optional chaining inside paths: `{{ order?.customer?.name }}`
378
+ - Quoted arguments are string literals; parenthesized arguments evaluate as nested expressions
379
+
380
+ ```yaml
381
+ # ❌ eval where a built-in exists
382
+ disabled: "{{ eval status !== 'Pending' }}"
383
+ isHidden: "{{ eval selectedItems.length === 0 }}"
384
+ # ✅ built-in equivalents
385
+ disabled: "{{ !isEqual status 'Pending' }}"
386
+ isHidden: "{{ !any selectedItems }}"
387
+ ```
388
+
389
+ **When `eval` is genuinely needed, keep it human-readable.** Inline form is only for
390
+ trivial expressions. Anything longer (ternaries, method chains, or ~60+ characters)
391
+ MUST be written as a multiline YAML block scalar:
392
+
393
+ ```yaml
394
+ # Multiline eval — block scalar, one clause per line
395
+ color: >-
396
+ {{ eval
397
+ item.status === 'delayed' ? '#e53e3e'
398
+ : item.priority === 'high' ? '#ffa726'
399
+ : '#4ecdc4'
400
+ }}
401
+ label: >-
402
+ {{ eval
403
+ attachments.items
404
+ .map(x => x.fileName)
405
+ .join(', ')
406
+ }}
407
+ ```
408
+
409
+ Multiline eval rules:
410
+ - **Always use `>-` (folded, strip)** — never `>`, `|`, or `|+`. Those keep a trailing
411
+ newline, so the value no longer ends with `}}` and the engine silently falls back to
412
+ string interpolation instead of evaluating the expression.
413
+ - The block must contain exactly one `{{ eval ... }}` and nothing else — surrounding text
414
+ or a second `{{ }}` also switches the engine to string interpolation.
415
+ - Block scalars need no YAML quoting or escaping — use natural JavaScript quotes
416
+ (`'` or `"`) inside the expression.
417
+ - Put `{{ eval` on the first line, one clause per indented line, and `}}` on its own
418
+ closing line.
419
+
341
420
  ### Permissions
342
421
  ```yaml
343
422
  permission: "ModuleName/Read" # Single string (PascalCase/Action)
@@ -474,7 +553,7 @@ npx cxtms app release -m "Add warehouse locations module" --org 42
474
553
  - Component names: Module/Component pattern (e.g., `WarehouseLocations/List`)
475
554
  - Route paths: kebab-case (e.g., `/warehouse-locations`)
476
555
  - Permission names: PascalCase with slashes (e.g., `WarehouseLocations/Read`, `System/Contacts/Update`)
477
- 4. **Template expressions** use `{{ expression }}` syntax (double curly braces)
556
+ 4. **Template expressions** use `{{ expression }}` syntax (double curly braces); prefer built-in functions (`isEqual`, `any`, `isNullOrEmpty`, `format`, …) over `eval`, and write non-trivial `eval` expressions as multiline block scalars (`>-`) — see Template expressions
478
557
  5. **Include filePath** property pointing to the YAML file location
479
558
  6. **Set proper entityKind** when defining entities (Order, Contact, OrderEntity, AccountingTransaction, Calendar, CalendarEvent, Other)
480
559
  7. **DataGrid options** requires ALL properties: query, rootEntityName, entityKeys, navigationType, enableDynamicGrid, enableViews, enableSearch, enablePagination, enableColumns, enableFilter, defaultView, onRowClick
@@ -314,7 +314,7 @@ props:
314
314
  - navigate: "orders/{{ item.id }}"
315
315
  - label: "Delete"
316
316
  icon: trash
317
- disabled: "{{ eval item.status === 'Completed' }}"
317
+ disabled: "{{ isEqual item.status 'Completed' }}"
318
318
  onClick:
319
319
  - confirm: { title: { en-US: "Delete?" } }
320
320
  onClick:
@@ -477,21 +477,6 @@ props:
477
477
 
478
478
  Compact theme-aware badge. Prefer semantic variants for new modules; the dot is opt-in.
479
479
 
480
- ## avatar, infoLine, and progressBar
481
-
482
- Use these template-aware display components for compact entity cards and planner headers.
483
-
484
- ```yaml
485
- - component: avatar
486
- props: { name: '{{ row.record.name }}', colorSeed: '{{ row.record.id }}', size: 40 }
487
- - component: infoLine
488
- props: { icon: tabler-phone, value: '{{ row.record.phone }}', href: 'tel:{{ row.record.phone }}' }
489
- - component: progressBar
490
- props: { items: '{{ row.items }}', completedPath: status, completedValue: Completed }
491
- ```
492
-
493
- `avatar` derives initials from names/email and uses `src` as an optional image. `infoLine` renders nothing for an empty value and only links allowlisted URL schemes. `progressBar` accepts direct `value`/`max` or derives counts from `items`; choose `count`/`percent` and `linear`/`circular`/`segmented` display modes. On mobile, `activeColor` and `trackColor` customize segmented progress; `tooltip` is exposed to assistive technology rather than hover UI.
494
-
495
480
  **Props:**
496
481
  | Prop | Type | Description |
497
482
  |------|------|-------------|
@@ -534,6 +519,104 @@ props:
534
519
 
535
520
  ---
536
521
 
522
+ ## avatar
523
+
524
+ Template-aware display component for compact entity cards and planner headers. Circular avatar with automatic two-letter initials. Sources tried in priority order: `src` image → `firstName` + `lastName` → `name` → `email` → placeholder user icon. Color is picked deterministically from the theme palette by hashing `colorSeed`.
525
+
526
+ **Props (all template-parsed):**
527
+ | Prop | Type | Description |
528
+ |------|------|-------------|
529
+ | `src` | `string` | Image URL; falls back to initials when empty or broken |
530
+ | `name` | `string` | Full name: `Andre Kovac` → AK, `Kovac, Andre` → AK, `Madonna` → MA |
531
+ | `firstName` / `lastName` | `string` | Explicit name parts (organization user form); take priority over `name` |
532
+ | `email` | `string` | Last text fallback: local part, digits dropped (`andre.kovac@…` → AK) |
533
+ | `colorSeed` | `string\|number` | Stable color seed — use an id (`contactId`, user id), not the name |
534
+ | `color` | `string` | Pin a theme color: `primary` \| `secondary` \| `error` \| `warning` \| `info` \| `success` |
535
+ | `bgcolor` / `textColor` | `string` | Explicit CSS color overrides |
536
+ | `size` | `number` | Diameter in px, default 40 |
537
+ | `variant` | `string` | `circular` (default) \| `rounded` \| `square` |
538
+ | `skin` | `string` | `light` (default, tinted bg) \| `filled` \| `light-static` |
539
+ | `tooltip` | `string\|localized` | Tooltip on hover |
540
+ | `onClick` | `Action[]` | Actions on click; when present the avatar is clickable |
541
+
542
+ ```yaml
543
+ component: avatar
544
+ name: driverLaneAvatar
545
+ props:
546
+ name: '{{ row.record.name }}'
547
+ colorSeed: '{{ row.record.contactId }}'
548
+ size: 40
549
+ ```
550
+
551
+ ---
552
+
553
+ ## infoLine
554
+
555
+ Compact "icon + value" line for entity cards (phone, email, truck, container…). Abstract: YAML assembles the value string, the component only renders it. **An empty value renders nothing**, so optional lines can be declared unconditionally.
556
+
557
+ **Props (all template-parsed):**
558
+ | Prop | Type | Description |
559
+ |------|------|-------------|
560
+ | `icon` | `string` | Icon class: `tabler-phone`, `tabler-mail`, `tabler-truck`, `tabler-container`, FA names |
561
+ | `iconColor` | `string` | Theme path or CSS color, default `text.secondary` |
562
+ | `value` | `string` | The text; assemble separators in YAML: `'{{ n }} • {{ size }}'`. Empty → not rendered |
563
+ | `href` | `string` | Renders the value as a link: `tel:{{ … }}`, `mailto:{{ … }}`, or a URL; only allowlisted URL schemes are linked |
564
+ | `onClick` | `Action[]` | Actions on click (alternative to href) |
565
+ | `truncate` | `boolean` | Ellipsis on overflow; tooltip defaults to the full value |
566
+ | `tooltip` | `string\|localized` | Explicit tooltip, overrides the truncate default |
567
+ | `variant` | `string` | `default` \| `chip` — bordered rounded container |
568
+
569
+ ```yaml
570
+ - component: infoLine
571
+ name: driverPhone
572
+ props:
573
+ icon: tabler-phone
574
+ value: '{{ row.record.phoneNumber }}'
575
+ href: 'tel:{{ row.record.phoneNumber }}'
576
+ - component: infoLine
577
+ name: containerChip
578
+ props:
579
+ icon: tabler-container
580
+ value: '{{ container.number }} • {{ container.size }}'
581
+ variant: chip
582
+ truncate: true
583
+ ```
584
+
585
+ ---
586
+
587
+ ## progressBar
588
+
589
+ Abstract progress bar. Feed it `value`/`max` directly, or an `items` array with a declarative completed predicate — what the numbers mean (moves, stops, hours…) is the module's business. **max 0 or missing → nothing renders**, so it can be declared unconditionally.
590
+
591
+ **Props (all template-parsed):**
592
+ | Prop | Type | Description |
593
+ |------|------|-------------|
594
+ | `value` | `number\|string` | Completed amount; overrides the derived count |
595
+ | `max` | `number\|string` | Total; defaults to `items.length` when `items` is set |
596
+ | `items` | `string` | Single-chunk template → array, e.g. `'{{ row.items }}'` |
597
+ | `completedPath` | `string` | Dot path inside each item, e.g. `orderMove.orderMoveStatus.statusStage` |
598
+ | `completedValue` | `string` | Match at the path counts as completed; omitted → any truthy value |
599
+ | `label` | `string\|localized` | Caption on the left, e.g. `Moves` |
600
+ | `showValue` | `boolean` | Value caption (`2/5`), default true |
601
+ | `valueFormat` | `string` | `count` (default) \| `percent` |
602
+ | `variant` | `string` | `linear` (default) \| `circular` \| `segmented` |
603
+ | `height` / `size` | `number` | Linear thickness (6) / circular diameter (40) |
604
+ | `color` | `string` | Theme name or CSS color; default primary → success at 100% |
605
+ | `activeColor` / `trackColor` | `string` | Mobile: completed-segment color / track or incomplete-segment color for the segmented variant |
606
+ | `tooltip` | `string\|localized` | Tooltip on hover (on mobile it is exposed to assistive technology rather than hover UI) |
607
+
608
+ ```yaml
609
+ - component: progressBar
610
+ name: laneMovesProgress
611
+ props:
612
+ label: Moves
613
+ items: '{{ row.items }}'
614
+ completedPath: orderMove.orderMoveStatus.statusStage
615
+ completedValue: Completed
616
+ ```
617
+
618
+ ---
619
+
537
620
  ## icon
538
621
 
539
622
  Icon renderer. Supports FontAwesome, Tabler, and Feather icons.
@@ -448,6 +448,32 @@ still control the displayed label and stored value.
448
448
  fields: ["name", "address_components", "formatted_address", "geometry", "place_id"]
449
449
  ```
450
450
 
451
+ **Attachment options (under `options`):**
452
+ | Prop | Type | Description |
453
+ |------|------|-------------|
454
+ | `parentId` / `parentType` | `string` | Primary parent of the uploaded attachment (`parentType` defaults to `Order`) |
455
+ | `links` | `{entityType, entityId}[]` or template | More entities to link the same upload to: `Order`, `Contact`, `Job`, `TrackingEvent`. Entries whose `entityType` or `entityId` resolves empty are skipped |
456
+ | `category` | `string` | Attachment category (web) |
457
+ | `allowMultiple` / `allowCamera` / `clearAfterUpload` | `boolean` | Upload behavior |
458
+ | `maxSize` / `allowedExtensions` | `number` / `string[]` | File limits |
459
+ | `onUploaded` | actions | Runs after each upload; `attachment` is in scope |
460
+
461
+ ```yaml
462
+ # One upload linked to the order (parent) and a tracking event
463
+ - component: field
464
+ name: eventPhoto
465
+ props:
466
+ type: attachment
467
+ options:
468
+ parentType: Order
469
+ parentId: "{{ orderId }}"
470
+ links:
471
+ - entityType: TrackingEvent
472
+ entityId: "{{ trackingEventId }}"
473
+ ```
474
+
475
+ `Attachments/ListAttachments` (cx-app-core) takes the same `links` prop next to `parentId`/`parentType` and passes it to its upload field; its grid still lists by `parentId`/`parentType`.
476
+
451
477
  **Events:**
452
478
  | Event | Description |
453
479
  |-------|-------------|
@@ -567,7 +593,7 @@ still control the displayed label and stored value.
567
593
  type: textarea
568
594
  label: { en-US: "Notes" }
569
595
  rows: 4
570
- disabled: "{{ eval !canEdit }}"
596
+ disabled: "{{ !canEdit }}"
571
597
  ```
572
598
 
573
599
  ---
@@ -83,7 +83,7 @@ MUI Button with icon, label, loading state, and action dispatch.
83
83
  icon: check-circle
84
84
  options:
85
85
  variant: success
86
- disabled: "{{ eval status !== 'Pending' }}"
86
+ disabled: "{{ !isEqual status 'Pending' }}"
87
87
  onClick:
88
88
  - mutation:
89
89
  command: "mutation($id: Int!) { approveOrder(id: $id) { success } }"
@@ -131,7 +131,7 @@ props:
131
131
  - dialog:
132
132
  component: Module/ImportDialog
133
133
  - label: { en-US: "Archive All" }
134
- disabled: "{{ eval selectedItems.length === 0 }}"
134
+ disabled: "{{ !any selectedItems }}"
135
135
  onClick:
136
136
  - confirm: { title: { en-US: "Archive?" }, message: { en-US: "Archive selected items?" } }
137
137
  ```
@@ -216,7 +216,7 @@ Programmatic navigation — renders nothing (or spinner with delay).
216
216
  component: redirect
217
217
  name: createRedirect
218
218
  props:
219
- condition: "{{ eval !id }}"
219
+ condition: "{{ !id }}"
220
220
  path: "/orders/create"
221
221
 
222
222
  # External redirect with delay
@@ -468,7 +468,7 @@ children:
468
468
  - name: advanced
469
469
  props:
470
470
  label: { en-US: "Advanced" }
471
- isHidden: "{{ eval !isAdmin }}"
471
+ isHidden: "{{ !isAdmin }}"
472
472
  children:
473
473
  - component: field
474
474
  name: config
@@ -46,6 +46,7 @@ npx cxtms create workflow <name> --template <template>
46
46
  | `webhook` | HTTP endpoint for external callers |
47
47
  | `public-api` | REST API endpoint with OpenAPI docs |
48
48
  | `mcp-tool` | Synchronous tool exposed through the organization MCP server |
49
+ | `agent` | AI agent that calls other workflows as tools (workflowType: Agent) |
49
50
 
50
51
  ### Step 2: Read the generated file
51
52
 
@@ -105,7 +106,7 @@ workflow:
105
106
  isActive: true
106
107
  enableAudit: true
107
108
  filePath: "workflows/<name>.yaml"
108
- workflowType: Document | Quote | Flow | Webhook | PublicApi | McpTool | McpResource | McpPrompt # omit for standard process workflows
109
+ workflowType: Document | Quote | Flow | Webhook | PublicApi | McpTool | McpResource | McpPrompt | Agent | EmailTemplate | RulesTariff # omit for standard process workflows
109
110
  runAs: "system" # Optional elevated permissions
110
111
  tags: ["tag1", "tag2"]
111
112
  concurrency: # Optional
@@ -342,6 +343,7 @@ Implicit variable: `iteration` (zero-based).
342
343
  | Template Expressions & Value Directives | `!cat skills/cxtms-workflow-builder/ref-expressions-template.md` |
343
344
  | NCalc Expressions & Functions | `!cat skills/cxtms-workflow-builder/ref-expressions-ncalc.md` |
344
345
  | Flow Workflows (state machines) | `!cat skills/cxtms-workflow-builder/ref-flow.md` |
346
+ | Agent Workflows (LLM agents) | `!cat skills/cxtms-workflow-builder/ref-agent.md` |
345
347
 
346
348
  ## Dynamic Schema Access (load on demand)
347
349