@hotmeshio/long-tail 0.9.5 → 0.10.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 (255) hide show
  1. package/README.md +22 -93
  2. package/build/api/escalations/helpers.d.ts +27 -0
  3. package/build/api/escalations/helpers.js +34 -0
  4. package/build/api/escalations/list.d.ts +2 -0
  5. package/build/api/escalations/list.js +13 -3
  6. package/build/api/escalations/resolve.js +16 -4
  7. package/build/routes/escalations/list.js +2 -0
  8. package/build/services/escalation/crud.d.ts +1 -1
  9. package/build/services/escalation/crud.js +6 -2
  10. package/build/services/escalation/facet-sql.d.ts +8 -1
  11. package/build/services/escalation/facet-sql.js +29 -1
  12. package/build/types/facets.d.ts +9 -0
  13. package/dashboard/dist/assets/{AdminDashboard-PvD1IObg.js → AdminDashboard-DQS74RL0.js} +2 -2
  14. package/dashboard/dist/assets/{AdminDashboard-PvD1IObg.js.map → AdminDashboard-DQS74RL0.js.map} +1 -1
  15. package/dashboard/dist/assets/{AgentConfigPage-BB9NCIiK.js → AgentConfigPage-CSMCrFR4.js} +7 -7
  16. package/dashboard/dist/assets/{AgentConfigPage-BB9NCIiK.js.map → AgentConfigPage-CSMCrFR4.js.map} +1 -1
  17. package/dashboard/dist/assets/{AgentDetailPage-B570UVLx.js → AgentDetailPage-CDiopQ7D.js} +3 -3
  18. package/dashboard/dist/assets/{AgentDetailPage-B570UVLx.js.map → AgentDetailPage-CDiopQ7D.js.map} +1 -1
  19. package/dashboard/dist/assets/{AgentsPage-CgUucHar.js → AgentsPage-CyUXe9n4.js} +2 -2
  20. package/dashboard/dist/assets/{AgentsPage-CgUucHar.js.map → AgentsPage-CyUXe9n4.js.map} +1 -1
  21. package/dashboard/dist/assets/AvailableEscalationsPage-AiuiLcz-.js +2 -0
  22. package/dashboard/dist/assets/AvailableEscalationsPage-AiuiLcz-.js.map +1 -0
  23. package/dashboard/dist/assets/BotPicker-BuCGvEed.js +2 -0
  24. package/dashboard/dist/assets/BotPicker-BuCGvEed.js.map +1 -0
  25. package/dashboard/dist/assets/CapabilitiesPage-nTv4STcG.js +2 -0
  26. package/dashboard/dist/assets/{CapabilitiesPage-C8DL3cDA.js.map → CapabilitiesPage-nTv4STcG.js.map} +1 -1
  27. package/dashboard/dist/assets/{CollapsibleSection-CDdlmdb6.js → CollapsibleSection-CkOks9Cf.js} +2 -2
  28. package/dashboard/dist/assets/{CollapsibleSection-CDdlmdb6.js.map → CollapsibleSection-CkOks9Cf.js.map} +1 -1
  29. package/dashboard/dist/assets/{ConfirmDeleteModal-D9_1b4MW.js → ConfirmDeleteModal-C0x0HcHX.js} +2 -2
  30. package/dashboard/dist/assets/{ConfirmDeleteModal-D9_1b4MW.js.map → ConfirmDeleteModal-C0x0HcHX.js.map} +1 -1
  31. package/dashboard/dist/assets/CountdownTimer-DdKL8Ryt.js +2 -0
  32. package/dashboard/dist/assets/CountdownTimer-DdKL8Ryt.js.map +1 -0
  33. package/dashboard/dist/assets/CredentialsPage-BwA7k9Cb.js +2 -0
  34. package/dashboard/dist/assets/{CredentialsPage-Bt7tQHlp.js.map → CredentialsPage-BwA7k9Cb.js.map} +1 -1
  35. package/dashboard/dist/assets/{CronLabel-DbwuLMO0.js → CronLabel-jC_i-Lh9.js} +2 -2
  36. package/dashboard/dist/assets/{CronLabel-DbwuLMO0.js.map → CronLabel-jC_i-Lh9.js.map} +1 -1
  37. package/dashboard/dist/assets/{CustomDurationPicker-CKfgf650.js → CustomDurationPicker-F6bR2Q5A.js} +2 -2
  38. package/dashboard/dist/assets/{CustomDurationPicker-CKfgf650.js.map → CustomDurationPicker-F6bR2Q5A.js.map} +1 -1
  39. package/dashboard/dist/assets/{DropZone-C1TpkVQG.js → DropZone-zxH-9B8c.js} +2 -2
  40. package/dashboard/dist/assets/{DropZone-C1TpkVQG.js.map → DropZone-zxH-9B8c.js.map} +1 -1
  41. package/dashboard/dist/assets/{ElapsedCell-CgpTWk8N.js → ElapsedCell-DZ3RTZFc.js} +2 -2
  42. package/dashboard/dist/assets/{ElapsedCell-CgpTWk8N.js.map → ElapsedCell-DZ3RTZFc.js.map} +1 -1
  43. package/dashboard/dist/assets/{EscalationListSchemaPage-CxFxPz4C.js → EscalationListSchemaPage-CaMydHIO.js} +3 -3
  44. package/dashboard/dist/assets/{EscalationListSchemaPage-CxFxPz4C.js.map → EscalationListSchemaPage-CaMydHIO.js.map} +1 -1
  45. package/dashboard/dist/assets/{EscalationSchemaPage-CXvjwJ_I.js → EscalationSchemaPage-BlMx2WE6.js} +3 -3
  46. package/dashboard/dist/assets/{EscalationSchemaPage-CXvjwJ_I.js.map → EscalationSchemaPage-BlMx2WE6.js.map} +1 -1
  47. package/dashboard/dist/assets/{EscalationsOverview-BY4dLou7.js → EscalationsOverview-DcMkbiSo.js} +2 -2
  48. package/dashboard/dist/assets/{EscalationsOverview-BY4dLou7.js.map → EscalationsOverview-DcMkbiSo.js.map} +1 -1
  49. package/dashboard/dist/assets/{EventTable-MFAHeFTY.js → EventTable-CuZ2Q_wH.js} +2 -2
  50. package/dashboard/dist/assets/{EventTable-MFAHeFTY.js.map → EventTable-CuZ2Q_wH.js.map} +1 -1
  51. package/dashboard/dist/assets/FilterBar-B5Vk5tBF.js +2 -0
  52. package/dashboard/dist/assets/FilterBar-B5Vk5tBF.js.map +1 -0
  53. package/dashboard/dist/assets/{GraphInvokePage-7T-6qC2N.js → GraphInvokePage-CXElQqcV.js} +2 -2
  54. package/dashboard/dist/assets/{GraphInvokePage-7T-6qC2N.js.map → GraphInvokePage-CXElQqcV.js.map} +1 -1
  55. package/dashboard/dist/assets/HomePage-DcWfjZA-.js +2 -0
  56. package/dashboard/dist/assets/HomePage-DcWfjZA-.js.map +1 -0
  57. package/dashboard/dist/assets/ListToolbar-Dmd1UiM-.js +2 -0
  58. package/dashboard/dist/assets/{ListToolbar-DmRE3_dX.js.map → ListToolbar-Dmd1UiM-.js.map} +1 -1
  59. package/dashboard/dist/assets/{McpOverview-BukcrbtX.js → McpOverview-Cul2LFEw.js} +2 -2
  60. package/dashboard/dist/assets/{McpOverview-BukcrbtX.js.map → McpOverview-Cul2LFEw.js.map} +1 -1
  61. package/dashboard/dist/assets/{McpQueryDetailPage-BVx-cGFZ.js → McpQueryDetailPage-BVlQ0WBa.js} +2 -2
  62. package/dashboard/dist/assets/{McpQueryDetailPage-BVx-cGFZ.js.map → McpQueryDetailPage-BVlQ0WBa.js.map} +1 -1
  63. package/dashboard/dist/assets/{McpQueryPage-DPXnA6Wa.js → McpQueryPage-B7PmO_V-.js} +2 -2
  64. package/dashboard/dist/assets/{McpQueryPage-DPXnA6Wa.js.map → McpQueryPage-B7PmO_V-.js.map} +1 -1
  65. package/dashboard/dist/assets/{McpRunDetailPage-D3dcICPJ.js → McpRunDetailPage-B1RhxxlU.js} +2 -2
  66. package/dashboard/dist/assets/{McpRunDetailPage-D3dcICPJ.js.map → McpRunDetailPage-B1RhxxlU.js.map} +1 -1
  67. package/dashboard/dist/assets/{McpRunsPage-CzISyj7e.js → McpRunsPage-DlSrpxiv.js} +2 -2
  68. package/dashboard/dist/assets/{McpRunsPage-CzISyj7e.js.map → McpRunsPage-DlSrpxiv.js.map} +1 -1
  69. package/dashboard/dist/assets/Modal-D4afnAjc.js +2 -0
  70. package/dashboard/dist/assets/Modal-D4afnAjc.js.map +1 -0
  71. package/dashboard/dist/assets/{NamespacePill--ikKtMvx.js → NamespacePill-BQNMfY0m.js} +2 -2
  72. package/dashboard/dist/assets/{NamespacePill--ikKtMvx.js.map → NamespacePill-BQNMfY0m.js.map} +1 -1
  73. package/dashboard/dist/assets/OperationsPage-vm_ByX41.js +2 -0
  74. package/dashboard/dist/assets/OperationsPage-vm_ByX41.js.map +1 -0
  75. package/dashboard/dist/assets/OperatorDashboard-zviIxH-0.js +2 -0
  76. package/dashboard/dist/assets/{OperatorDashboard-C_bMBGyy.js.map → OperatorDashboard-zviIxH-0.js.map} +1 -1
  77. package/dashboard/dist/assets/{PageHeader-CiFhzGc0.js → PageHeader-BIY1YvKR.js} +2 -2
  78. package/dashboard/dist/assets/{PageHeader-CiFhzGc0.js.map → PageHeader-BIY1YvKR.js.map} +1 -1
  79. package/dashboard/dist/assets/{PageHeaderWithStats-_zOnWAhq.js → PageHeaderWithStats-TUyglVW0.js} +2 -2
  80. package/dashboard/dist/assets/{PageHeaderWithStats-_zOnWAhq.js.map → PageHeaderWithStats-TUyglVW0.js.map} +1 -1
  81. package/dashboard/dist/assets/{ProcessDetailPage-BVR41sYI.js → ProcessDetailPage-txiMHbMI.js} +2 -2
  82. package/dashboard/dist/assets/{ProcessDetailPage-BVR41sYI.js.map → ProcessDetailPage-txiMHbMI.js.map} +1 -1
  83. package/dashboard/dist/assets/{ProcessesListPage-DAgFK_ln.js → ProcessesListPage-CRttlU3L.js} +2 -2
  84. package/dashboard/dist/assets/{ProcessesListPage-DAgFK_ln.js.map → ProcessesListPage-CRttlU3L.js.map} +1 -1
  85. package/dashboard/dist/assets/RoleDetailPage-DBIl_mhK.js +8 -0
  86. package/dashboard/dist/assets/RoleDetailPage-DBIl_mhK.js.map +1 -0
  87. package/dashboard/dist/assets/RolePill-4LO_CuO3.js +2 -0
  88. package/dashboard/dist/assets/RolePill-4LO_CuO3.js.map +1 -0
  89. package/dashboard/dist/assets/RolesPage-DzrSv6m-.js +2 -0
  90. package/dashboard/dist/assets/{RolesPage-dRSC725H.js.map → RolesPage-DzrSv6m-.js.map} +1 -1
  91. package/dashboard/dist/assets/RunAsSelector-CiLjlmIE.js +2 -0
  92. package/dashboard/dist/assets/{RunAsSelector-C36xqdKj.js.map → RunAsSelector-CiLjlmIE.js.map} +1 -1
  93. package/dashboard/dist/assets/{SlidePanel-BcWKelpn.js → SlidePanel-DK4sWXmp.js} +2 -2
  94. package/dashboard/dist/assets/{SlidePanel-BcWKelpn.js.map → SlidePanel-DK4sWXmp.js.map} +1 -1
  95. package/dashboard/dist/assets/StickyPagination-Di6b2GDE.js +2 -0
  96. package/dashboard/dist/assets/{StickyPagination-D_HhyJ5V.js.map → StickyPagination-Di6b2GDE.js.map} +1 -1
  97. package/dashboard/dist/assets/{StreamMessageDetail-C39lLYKC.js → StreamMessageDetail-BbkU45X_.js} +2 -2
  98. package/dashboard/dist/assets/{StreamMessageDetail-C39lLYKC.js.map → StreamMessageDetail-BbkU45X_.js.map} +1 -1
  99. package/dashboard/dist/assets/{SwimlaneTimeline-dlaJS-DC.js → SwimlaneTimeline-BzsvdstG.js} +2 -2
  100. package/dashboard/dist/assets/{SwimlaneTimeline-dlaJS-DC.js.map → SwimlaneTimeline-BzsvdstG.js.map} +1 -1
  101. package/dashboard/dist/assets/{TagInput-BaFJCq9A.js → TagInput-CK-JhBG2.js} +2 -2
  102. package/dashboard/dist/assets/{TagInput-BaFJCq9A.js.map → TagInput-CK-JhBG2.js.map} +1 -1
  103. package/dashboard/dist/assets/{TaskDetailPage-DAnT9c0v.js → TaskDetailPage-Biikk9o_.js} +2 -2
  104. package/dashboard/dist/assets/{TaskDetailPage-DAnT9c0v.js.map → TaskDetailPage-Biikk9o_.js.map} +1 -1
  105. package/dashboard/dist/assets/{TaskQueuePill-D9e16qkU.js → TaskQueuePill-C_Vo2Clc.js} +2 -2
  106. package/dashboard/dist/assets/{TaskQueuePill-D9e16qkU.js.map → TaskQueuePill-C_Vo2Clc.js.map} +1 -1
  107. package/dashboard/dist/assets/{TasksListPage-CkbK3vdj.js → TasksListPage-t9WAft9-.js} +2 -2
  108. package/dashboard/dist/assets/{TasksListPage-CkbK3vdj.js.map → TasksListPage-t9WAft9-.js.map} +1 -1
  109. package/dashboard/dist/assets/{TimeAgo-drImakaN.js → TimeAgo-I7X6jdne.js} +2 -2
  110. package/dashboard/dist/assets/{TimeAgo-drImakaN.js.map → TimeAgo-I7X6jdne.js.map} +1 -1
  111. package/dashboard/dist/assets/{TimestampCell-CpWhUW0W.js → TimestampCell-Cex9nr0t.js} +2 -2
  112. package/dashboard/dist/assets/{TimestampCell-CpWhUW0W.js.map → TimestampCell-Cex9nr0t.js.map} +1 -1
  113. package/dashboard/dist/assets/ToolPill-BCi101Jh.js +2 -0
  114. package/dashboard/dist/assets/{ToolPill-Enc3nyic.js.map → ToolPill-BCi101Jh.js.map} +1 -1
  115. package/dashboard/dist/assets/{ToolTestPanel-PvMw4dIw.js → ToolTestPanel--MGaDY-1.js} +2 -2
  116. package/dashboard/dist/assets/{ToolTestPanel-PvMw4dIw.js.map → ToolTestPanel--MGaDY-1.js.map} +1 -1
  117. package/dashboard/dist/assets/{TopicDetailPage-DQa0EuM9.js → TopicDetailPage-B8pHHweA.js} +3 -3
  118. package/dashboard/dist/assets/{TopicDetailPage-DQa0EuM9.js.map → TopicDetailPage-B8pHHweA.js.map} +1 -1
  119. package/dashboard/dist/assets/{TopicsPage-OzE8IWZf.js → TopicsPage-tiOEQz2m.js} +2 -2
  120. package/dashboard/dist/assets/{TopicsPage-OzE8IWZf.js.map → TopicsPage-tiOEQz2m.js.map} +1 -1
  121. package/dashboard/dist/assets/{UserName-B-5Cnap4.js → UserName-Cdhyuv5K.js} +2 -2
  122. package/dashboard/dist/assets/{UserName-B-5Cnap4.js.map → UserName-Cdhyuv5K.js.map} +1 -1
  123. package/dashboard/dist/assets/{WorkflowExecutionPage-s4H5TnRv.js → WorkflowExecutionPage-DD1BKj5k.js} +2 -2
  124. package/dashboard/dist/assets/{WorkflowExecutionPage-s4H5TnRv.js.map → WorkflowExecutionPage-DD1BKj5k.js.map} +1 -1
  125. package/dashboard/dist/assets/WorkflowPill-DHxrAkp3.js +2 -0
  126. package/dashboard/dist/assets/{WorkflowPill-S6VV2Eax.js.map → WorkflowPill-DHxrAkp3.js.map} +1 -1
  127. package/dashboard/dist/assets/{WorkflowsDashboard-BqMJDhpp.js → WorkflowsDashboard-BeWjDHYW.js} +2 -2
  128. package/dashboard/dist/assets/{WorkflowsDashboard-BqMJDhpp.js.map → WorkflowsDashboard-BeWjDHYW.js.map} +1 -1
  129. package/dashboard/dist/assets/{WorkflowsOverview-CVd-TybI.js → WorkflowsOverview-CkI-Pmj7.js} +2 -2
  130. package/dashboard/dist/assets/{WorkflowsOverview-CVd-TybI.js.map → WorkflowsOverview-CkI-Pmj7.js.map} +1 -1
  131. package/dashboard/dist/assets/{YamlWorkflowDetailPage-BshuXnvM.js → YamlWorkflowDetailPage-BQnO8-PQ.js} +2 -2
  132. package/dashboard/dist/assets/{YamlWorkflowDetailPage-BshuXnvM.js.map → YamlWorkflowDetailPage-BQnO8-PQ.js.map} +1 -1
  133. package/dashboard/dist/assets/{YamlWorkflowsPage-D2LRoAki.js → YamlWorkflowsPage-DjmNTQ5r.js} +2 -2
  134. package/dashboard/dist/assets/{YamlWorkflowsPage-D2LRoAki.js.map → YamlWorkflowsPage-DjmNTQ5r.js.map} +1 -1
  135. package/dashboard/dist/assets/{agents-CqeRO052.js → agents-2DaoprdB.js} +2 -2
  136. package/dashboard/dist/assets/{agents-CqeRO052.js.map → agents-2DaoprdB.js.map} +1 -1
  137. package/dashboard/dist/assets/{bots-yo56kQZI.js → bots-BdwLwMUg.js} +2 -2
  138. package/dashboard/dist/assets/{bots-yo56kQZI.js.map → bots-BdwLwMUg.js.map} +1 -1
  139. package/dashboard/dist/assets/{capabilities-C-LyQnOQ.js → capabilities-BsuFeK9J.js} +2 -2
  140. package/dashboard/dist/assets/{capabilities-C-LyQnOQ.js.map → capabilities-BsuFeK9J.js.map} +1 -1
  141. package/dashboard/dist/assets/{controlplane-cpRK9tBf.js → controlplane-CxKOrcFO.js} +2 -2
  142. package/dashboard/dist/assets/{controlplane-cpRK9tBf.js.map → controlplane-CxKOrcFO.js.map} +1 -1
  143. package/dashboard/dist/assets/escalation-columns-y5SQ2Ulh.js +2 -0
  144. package/dashboard/dist/assets/escalation-columns-y5SQ2Ulh.js.map +1 -0
  145. package/dashboard/dist/assets/index-BC1NxetR.js +2 -0
  146. package/dashboard/dist/assets/{index-JMRKuqah.js.map → index-BC1NxetR.js.map} +1 -1
  147. package/dashboard/dist/assets/index-BVZINC_p.js +2 -0
  148. package/dashboard/dist/assets/{index-D3_Kc337.js.map → index-BVZINC_p.js.map} +1 -1
  149. package/dashboard/dist/assets/{index-DQgRu4cd.js → index-BXbt5NHG.js} +2 -2
  150. package/dashboard/dist/assets/{index-DQgRu4cd.js.map → index-BXbt5NHG.js.map} +1 -1
  151. package/dashboard/dist/assets/{index-C15QKc-V.js → index-BYEkfaUq.js} +2 -2
  152. package/dashboard/dist/assets/{index-C15QKc-V.js.map → index-BYEkfaUq.js.map} +1 -1
  153. package/dashboard/dist/assets/index-B_68lp-B.js +5 -0
  154. package/dashboard/dist/assets/index-B_68lp-B.js.map +1 -0
  155. package/dashboard/dist/assets/index-BeivyVtT.css +1 -0
  156. package/dashboard/dist/assets/{index-DNKzmRA3.js → index-CRBXgMsh.js} +2 -2
  157. package/dashboard/dist/assets/{index-DNKzmRA3.js.map → index-CRBXgMsh.js.map} +1 -1
  158. package/dashboard/dist/assets/{index-D_FpjN49.js → index-Cfq3UKd8.js} +2 -2
  159. package/dashboard/dist/assets/{index-D_FpjN49.js.map → index-Cfq3UKd8.js.map} +1 -1
  160. package/dashboard/dist/assets/{index-D9GgDI2q.js → index-DJ5nhcGB.js} +2 -2
  161. package/dashboard/dist/assets/{index-D9GgDI2q.js.map → index-DJ5nhcGB.js.map} +1 -1
  162. package/dashboard/dist/assets/{index-B9YOp7_Z.js → index-DVLBkcoD.js} +4 -4
  163. package/dashboard/dist/assets/index-DVLBkcoD.js.map +1 -0
  164. package/dashboard/dist/assets/index-DjI2AUbn.js +5 -0
  165. package/dashboard/dist/assets/{index-DpYsAAsj.js.map → index-DjI2AUbn.js.map} +1 -1
  166. package/dashboard/dist/assets/{index-d8dDpRIs.js → index-DyS2DHqy.js} +2 -2
  167. package/dashboard/dist/assets/{index-d8dDpRIs.js.map → index-DyS2DHqy.js.map} +1 -1
  168. package/dashboard/dist/assets/index-H6Vp-gPG.js +2 -0
  169. package/dashboard/dist/assets/{index-CvyZWk2N.js.map → index-H6Vp-gPG.js.map} +1 -1
  170. package/dashboard/dist/assets/index-YPLTjeli.js +63 -0
  171. package/dashboard/dist/assets/index-YPLTjeli.js.map +1 -0
  172. package/dashboard/dist/assets/{knowledge-DSTPZRsM.js → knowledge-CLnYcq0Y.js} +2 -2
  173. package/dashboard/dist/assets/{knowledge-DSTPZRsM.js.map → knowledge-CLnYcq0Y.js.map} +1 -1
  174. package/dashboard/dist/assets/{mcp-DPzcauBf.js → mcp-C1-iXkg7.js} +2 -2
  175. package/dashboard/dist/assets/{mcp-DPzcauBf.js.map → mcp-C1-iXkg7.js.map} +1 -1
  176. package/dashboard/dist/assets/{mcp-query-CYLlRjwY.js → mcp-query-DcyDxuGY.js} +2 -2
  177. package/dashboard/dist/assets/{mcp-query-CYLlRjwY.js.map → mcp-query-DcyDxuGY.js.map} +1 -1
  178. package/dashboard/dist/assets/{pipelines-BtfaUf7f.js → pipelines-BvG5D6bV.js} +2 -2
  179. package/dashboard/dist/assets/{pipelines-BtfaUf7f.js.map → pipelines-BvG5D6bV.js.map} +1 -1
  180. package/dashboard/dist/assets/{tasks-bMka9A7P.js → tasks-qwh8q5q7.js} +2 -2
  181. package/dashboard/dist/assets/{tasks-bMka9A7P.js.map → tasks-qwh8q5q7.js.map} +1 -1
  182. package/dashboard/dist/assets/{topics-CFcuDwn2.js → topics-MLKKA-Ql.js} +2 -2
  183. package/dashboard/dist/assets/{topics-CFcuDwn2.js.map → topics-MLKKA-Ql.js.map} +1 -1
  184. package/dashboard/dist/assets/{useEventHooks-DrtLEQzK.js → useEventHooks-B8uNFB-l.js} +2 -2
  185. package/dashboard/dist/assets/{useEventHooks-DrtLEQzK.js.map → useEventHooks-B8uNFB-l.js.map} +1 -1
  186. package/dashboard/dist/assets/{useNamespace-BUb0LZ9l.js → useNamespace-DjZmztJM.js} +2 -2
  187. package/dashboard/dist/assets/{useNamespace-BUb0LZ9l.js.map → useNamespace-DjZmztJM.js.map} +1 -1
  188. package/dashboard/dist/assets/{useYamlActivityEvents-6DPlPdg0.js → useYamlActivityEvents-DIvK20uI.js} +2 -2
  189. package/dashboard/dist/assets/{useYamlActivityEvents-6DPlPdg0.js.map → useYamlActivityEvents-DIvK20uI.js.map} +1 -1
  190. package/dashboard/dist/assets/{users-FTnxEv_t.js → users-CVjajeDg.js} +2 -2
  191. package/dashboard/dist/assets/{users-FTnxEv_t.js.map → users-CVjajeDg.js.map} +1 -1
  192. package/dashboard/dist/assets/{vendor-icons-DPmIjhZB.js → vendor-icons-Qk9qLjF_.js} +131 -126
  193. package/dashboard/dist/assets/vendor-icons-Qk9qLjF_.js.map +1 -0
  194. package/dashboard/dist/assets/{workflows-DgeQWLWK.js → workflows-BziTcJNq.js} +2 -2
  195. package/dashboard/dist/assets/{workflows-DgeQWLWK.js.map → workflows-BziTcJNq.js.map} +1 -1
  196. package/dashboard/dist/assets/{yaml-workflows-BjatB1t-.js → yaml-workflows-Dr-ZcE_0.js} +2 -2
  197. package/dashboard/dist/assets/{yaml-workflows-BjatB1t-.js.map → yaml-workflows-Dr-ZcE_0.js.map} +1 -1
  198. package/dashboard/dist/index.html +3 -3
  199. package/docs/dashboard.md +2 -2
  200. package/docs/faceted-routing.md +9 -0
  201. package/docs/hitl/escalation.md +206 -0
  202. package/docs/hitl/form.md +162 -0
  203. package/docs/hitl/iframe.md +170 -0
  204. package/docs/hitl/resolution.md +166 -0
  205. package/docs/hitl/roles.md +72 -0
  206. package/docs/hitl/x-lt-layout.md +167 -0
  207. package/docs/hitl/x-lt-list-schema.md +75 -0
  208. package/docs/hitl/x-lt-show-if.md +128 -0
  209. package/docs/hitl/x-lt-validation.md +168 -0
  210. package/docs/hitl/x-lt-widget.md +152 -0
  211. package/docs/hitl-guide.md +84 -1144
  212. package/docs/iam.md +1 -1
  213. package/package.json +2 -2
  214. package/dashboard/dist/assets/AvailableEscalationsPage-B1cCs4jO.js +0 -2
  215. package/dashboard/dist/assets/AvailableEscalationsPage-B1cCs4jO.js.map +0 -1
  216. package/dashboard/dist/assets/BotPicker-CYw4ZeAh.js +0 -2
  217. package/dashboard/dist/assets/BotPicker-CYw4ZeAh.js.map +0 -1
  218. package/dashboard/dist/assets/CapabilitiesPage-C8DL3cDA.js +0 -2
  219. package/dashboard/dist/assets/CountdownTimer-BrQl3JWm.js +0 -2
  220. package/dashboard/dist/assets/CountdownTimer-BrQl3JWm.js.map +0 -1
  221. package/dashboard/dist/assets/CredentialsPage-Bt7tQHlp.js +0 -2
  222. package/dashboard/dist/assets/FilterBar-DDIn8TOs.js +0 -2
  223. package/dashboard/dist/assets/FilterBar-DDIn8TOs.js.map +0 -1
  224. package/dashboard/dist/assets/HomePage-1PI7E3sw.js +0 -2
  225. package/dashboard/dist/assets/HomePage-1PI7E3sw.js.map +0 -1
  226. package/dashboard/dist/assets/ListToolbar-DmRE3_dX.js +0 -2
  227. package/dashboard/dist/assets/Modal-CSrxpXeM.js +0 -2
  228. package/dashboard/dist/assets/Modal-CSrxpXeM.js.map +0 -1
  229. package/dashboard/dist/assets/OperationsPage-BYth5Dxg.js +0 -2
  230. package/dashboard/dist/assets/OperationsPage-BYth5Dxg.js.map +0 -1
  231. package/dashboard/dist/assets/OperatorDashboard-C_bMBGyy.js +0 -2
  232. package/dashboard/dist/assets/RoleDetailPage-NxT8GaWV.js +0 -8
  233. package/dashboard/dist/assets/RoleDetailPage-NxT8GaWV.js.map +0 -1
  234. package/dashboard/dist/assets/RolePill-CUIJSgRq.js +0 -2
  235. package/dashboard/dist/assets/RolePill-CUIJSgRq.js.map +0 -1
  236. package/dashboard/dist/assets/RolesPage-dRSC725H.js +0 -2
  237. package/dashboard/dist/assets/RunAsSelector-C36xqdKj.js +0 -2
  238. package/dashboard/dist/assets/StickyPagination-D_HhyJ5V.js +0 -2
  239. package/dashboard/dist/assets/ToolPill-Enc3nyic.js +0 -2
  240. package/dashboard/dist/assets/WorkflowPill-S6VV2Eax.js +0 -2
  241. package/dashboard/dist/assets/escalation-columns-CSG3LG3F.js +0 -2
  242. package/dashboard/dist/assets/escalation-columns-CSG3LG3F.js.map +0 -1
  243. package/dashboard/dist/assets/index-B9YOp7_Z.js.map +0 -1
  244. package/dashboard/dist/assets/index-BJwHI2aX.js +0 -63
  245. package/dashboard/dist/assets/index-BJwHI2aX.js.map +0 -1
  246. package/dashboard/dist/assets/index-CvyZWk2N.js +0 -2
  247. package/dashboard/dist/assets/index-D3_Kc337.js +0 -2
  248. package/dashboard/dist/assets/index-D9I9XhrD.js +0 -5
  249. package/dashboard/dist/assets/index-D9I9XhrD.js.map +0 -1
  250. package/dashboard/dist/assets/index-DpYsAAsj.js +0 -5
  251. package/dashboard/dist/assets/index-JMRKuqah.js +0 -2
  252. package/dashboard/dist/assets/index-waKF7bKZ.css +0 -1
  253. package/dashboard/dist/assets/roles-CDi58HW9.js +0 -2
  254. package/dashboard/dist/assets/roles-CDi58HW9.js.map +0 -1
  255. package/dashboard/dist/assets/vendor-icons-DPmIjhZB.js.map +0 -1
@@ -1,10 +1,30 @@
1
- # Human-in-the-Loop (HITL) Guide
1
+ # Human-in-the-Loop (HITL)
2
2
 
3
- Build durable workflows that pause for human input and resume automatically when the human responds. Long-tail provides the full escalation lifecycle — claiming, routing, forms, resolution — so you focus on business logic and form design.
3
+ Build durable workflows that pause for human input and resume automatically when the human responds. Long-tail handles the full escalation lifecycle — claiming, routing, forms, resolution — so you focus on business logic and form design.
4
4
 
5
5
  ---
6
6
 
7
- ## Architecture Overview
7
+ ## Design Philosophy: The Form Is Data
8
+
9
+ The escalation is the unit of UI. The **role** decides who sees it, the **schema** decides what they see, the **version** decides which edition renders, and the **scope** decides how much of the queue is theirs. An escalation can be handled by any member of its role — a person working the dashboard or a service account resolving through the API — the role is the contract, not the kind of actor behind it. A workflow that assigns an escalation to a named user with a self-scoped membership produces a just-in-time form, scoped by RBAC — one person, one item, one versioned surface — without a line of frontend code.
10
+
11
+ This works because the form is data: a JSON Schema stored on the role, versioned in `lt_role_schemas`, snapshotted immutably on every edit, and pinnable per escalation. A schema-driven form is auditable (which edition did the resolver see?), validated by the platform (required, bounds, patterns, conditionals), and rendered consistently. The iframe viewport is the escape hatch for domains that need a fully custom surface — a WebGL editor, a PDF workbench — and it trades all of those platform guarantees for total control. Reach for the schema first; reach for the iframe when the domain demands it.
12
+
13
+ ### Choosing Your Surface
14
+
15
+ | You need | Use | Doc |
16
+ |----------|-----|-----|
17
+ | Typed fields, formats, required | Plain JSON Schema | [form.md](hitl/form.md) |
18
+ | Input guards (bounds, patterns, dynamic limits) | Validation keywords | [x-lt-validation.md](hitl/x-lt-validation.md) |
19
+ | Fields that appear based on another answer | `x-lt-showIf` | [x-lt-show-if.md](hitl/x-lt-show-if.md) |
20
+ | Sections, columns, ordering, side-panel help | Layout keywords | [x-lt-layout.md](hitl/x-lt-layout.md) |
21
+ | Runtime-driven items, files, signatures, SOP blocks | Widgets | [x-lt-widget.md](hitl/x-lt-widget.md) |
22
+ | A role-authored list page for the whole queue | List schema | [x-lt-list-schema.md](hitl/x-lt-list-schema.md) |
23
+ | A fully custom UI nothing above can express | Iframe viewport | [iframe.md](hitl/iframe.md) |
24
+
25
+ ---
26
+
27
+ ## Architecture
8
28
 
9
29
  ```
10
30
  Durable Workflow Long-tail Platform Dashboard
@@ -20,1166 +40,86 @@ Durable Workflow Long-tail Platform Dashboard
20
40
  ```
21
41
 
22
42
  1. **Workflow escalates** — creates an escalation record with a role, description, and optional form schema
23
- 2. **Platform routes** — the escalation appears in the dashboard for users with the matching role
24
- 3. **Human claims** — a user claims the work item (soft-lock with TTL)
25
- 4. **Human submits** — the form response is sent back as a signal to the paused workflow
26
- 5. **Workflow resumes** — continues execution with the human's input as the resolver payload
43
+ 2. **Platform routes** — the escalation appears in the queue of every member of the matching role: people see it in the dashboard, service accounts act on it through the API or MCP tools
44
+ 3. **Member claims** — a role member claims the work item (soft-lock with TTL)
45
+ 4. **Member submits** — the form response is sent back as a signal to the paused workflow
46
+ 5. **Workflow resumes** — continues execution with the resolver payload
27
47
 
28
48
  ---
29
49
 
30
- ## Creating Escalations
31
-
32
- ### Pattern 1: `conditionLT` Signal (Recommended)
33
-
34
- The workflow stays running and waits for a signal. Lightweight, no re-run needed. Two forms — prefer the atomic one.
35
-
36
- #### Atomic form (recommended)
37
-
38
- Pass an escalation config to `conditionLT`. The escalation row is written inside the workflow's Leg1 checkpoint — one commit, crash-safe: no separate create activity, no enrich step. `signal_key` is the resume key, so the dashboard resolve endpoint and `POST /escalations/resolve-by-signal-key` both resume *this* job in place, and `system.escalation.{id}.created` fires automatically.
39
-
40
- ```typescript
41
- import { conditionLT } from '@hotmeshio/long-tail';
42
-
43
- export async function approvalWorkflow(envelope: LTEnvelope) {
44
- const ctx = Durable.workflow.workflowInfo();
45
- const signalId = `approval-${ctx.workflowId}`;
46
-
47
- // One atomic expression: write the escalation in Leg1, then pause.
48
- const decision = await conditionLT<{ approved: boolean; notes?: string }>(signalId, {
49
- role: 'finance-reviewer',
50
- type: 'approval',
51
- subtype: 'budget-request',
52
- priority: 2,
53
- description: `Budget approval needed: $${envelope.data.amount}`,
54
- metadata: {
55
- form_schema: {
56
- title: 'Budget Approval',
57
- properties: {
58
- approved: { type: 'boolean', description: 'Approve this request?' },
59
- notes: { type: 'string', format: 'textarea' },
60
- },
61
- required: ['approved'],
62
- },
63
- },
64
- envelope: { data: envelope.data },
65
- timeout: '72h', // SLA: resume with false + expire the row if unresolved
66
- });
67
-
68
- if (decision === false) {
69
- // SLA timer fired first — the row is already status='expired' (engine-side,
70
- // atomic) and a late resolve returns already-expired. Branch to fallback.
71
- return { type: 'return' as const, data: { autoRejected: 'sla' } };
72
- }
73
- if (decision === null) {
74
- // escalation was cancelled (workflow terminated or explicit cancel)
75
- return { type: 'return' as const, data: { cancelled: true } };
76
- }
77
-
78
- if (decision.approved) {
79
- // ... proceed with approved flow ...
80
- } else {
81
- // ... handle rejection ...
82
- }
83
- }
84
- ```
85
-
86
- The `timeout` field makes the wait SLA-gated in the same single Leg1 write:
87
- one `conditionLT` call yields the worklist row AND the resume timer. Omit it
88
- for an open-ended wait.
89
-
90
- Two engine contracts worth building on:
91
-
92
- - **The row is complete from its first visible moment.** Every field of the
93
- config — including `metadata` facets — commits inside the Leg1 checkpoint.
94
- A claim-by-metadata router or a version-pinned facet (e.g. a
95
- `schema_version` the resolver UI renders) can trust every row it reads;
96
- there is no window where a row is visible but its metadata is still en route.
97
- - **Early signals are buffered.** A resolve that races ahead of the
98
- `condition()` registration (a fast webhook, a payload deposited before the
99
- workflow starts) is held as a pending signal and delivered when the wait
100
- registers — 10 minutes by default; pass `expire` to `signal()` (e.g. `'1h'`)
101
- when signaling early on purpose. Fan-out (`Promise.all` over many waits)
102
- scales the same way: buffering covers every signal that outruns its
103
- registration.
104
-
105
- #### Two-step form
106
-
107
- When you need to create the escalation separately — for example to enrich routing metadata before pausing — create it first, then wait:
108
-
109
- ```typescript
110
- import { conditionLT } from 'long-tail/orchestrator';
111
- import { ltCreateEscalation } from 'long-tail/activities';
112
-
113
- export async function approvalWorkflow(envelope: LTEnvelope) {
114
- // ... do initial work ...
115
-
116
- const signalId = `approval-${ctx.workflowId}`;
117
-
118
- // Create the escalation with a form schema
119
- await ltCreateEscalation({
120
- type: 'approval',
121
- subtype: 'budget-request',
122
- description: `Budget approval needed: $${envelope.amount}`,
123
- role: 'finance-reviewer',
124
- priority: 2,
125
- envelope: JSON.stringify(envelope),
126
- workflowId: ctx.workflowId,
127
- taskQueue: ctx.taskQueue,
128
- workflowType: 'approvalWorkflow',
129
- metadata: {
130
- signal_id: signalId,
131
- form_schema: {
132
- title: 'Budget Approval',
133
- description: 'Review the budget request and approve or reject.',
134
- properties: {
135
- approved: { type: 'boolean', description: 'Approve this request?' },
136
- notes: { type: 'string', format: 'textarea', description: 'Optional reviewer notes' },
137
- },
138
- required: ['approved'],
139
- },
140
- },
141
- });
142
-
143
- // Workflow pauses here until the human responds
144
- const decision = await conditionLT<{ approved: boolean; notes?: string }>(signalId);
145
-
146
- if (!decision) {
147
- // null = cancelled, false = timeout
148
- return { type: 'return' as const, data: { cancelled: true } };
149
- }
150
-
151
- if (decision.approved) {
152
- // ... proceed with approved flow ...
153
- } else {
154
- // ... handle rejection ...
155
- }
156
- }
157
- ```
158
-
159
- ### Pattern 2: Interceptor Return
160
-
161
- The workflow returns an escalation result. The interceptor handles creation. On resolution, the workflow is re-run with the resolver payload injected into the envelope.
162
-
163
- ```typescript
164
- export async function reviewWorkflow(envelope: LTEnvelope) {
165
- if (needsHumanReview(envelope)) {
166
- return {
167
- type: 'escalation',
168
- data: { document: envelope.documentUrl },
169
- message: 'Document requires human review before publishing',
170
- priority: 2,
171
- role: 'content-reviewer',
172
- };
173
- }
174
- // ... normal flow ...
175
- }
176
- ```
177
-
178
- ### Which Schema Renders the Form
179
-
180
- The resolver form resolves in order, most specific first:
181
-
182
- 1. **`metadata.form_schema`** — a full JSON Schema embedded on the escalation row. Use when different escalation points in the same workflow need different forms.
183
- 2. **The role's `form_schema`** — the versioned form every role provides. When the row carries a `metadata.schema_version` pin (set via `schemaVersion` in the `conditionLT` config), the form renders exactly that snapshot even after the role's schema changes; without a pin, the role's latest applies.
184
- 3. **Workflow config `resolver_schema`** — **deprecated** legacy fallback only, used when no role `form_schema` is available.
185
-
186
- ### Versioned Role Schemas
50
+ ## What Long-tail Provides
187
51
 
188
- Every save that changes a role's `form_schema` or `metadata_schema` appends an immutable snapshot to the version history (`lt_role_schemas`) and advances the role's current version. Escalations that need a guaranteed shape pin one:
189
-
190
- ```typescript
191
- const decision = await conditionLT<{ approved: boolean; lotNumber: string }>(signalId, {
192
- role: 'reviewer',
193
- description: instructions,
194
- schemaVersion: 3, // this row renders role schema v3, always
195
- });
196
- ```
197
-
198
- The pin travels as `metadata.schema_version` on the row (GIN-indexed, queryable like any facet). A pin that names a missing version fails at creation with a 400 — it never falls through to a different version. Without a pin, the role's latest schema always applies: workflow authors who don't care get the current form automatically; authors who depend on a specific field (say, a form that gained `lotNumber` in v3 and the workflow reads it back) pin the version and the round trip stays aligned.
52
+ When you author a HITL-backed workflow, the platform handles:
199
53
 
200
- `metadata.schema_version` also selects which `metadata_schema` validates the creation-time metadata bag on `POST /api/escalations`.
54
+ - **Escalation routing** role-based, priority-ordered work queues
55
+ - **Claim/release** — soft-lock with TTL; an extend prompt before expiry, a locked form after, and a resolve guard that rejects stale claims (see [resolution.md](hitl/resolution.md#claim-lifecycle))
56
+ - **Real-time updates** — NATS/Socket.IO events push changes to the dashboard instantly
57
+ - **Form rendering** — JSON Schema to rich form controls, no frontend code needed
58
+ - **Draft persistence** — form edits are saved locally per escalation and restored on return; cleared on submit or cancel
59
+ - **Accessible forms** — generated controls carry label association, error announcements, and keyboard-correct locking (see [form.md](hitl/form.md#accessibility))
60
+ - **Side panel** — help, AI analysis, metadata, context, and raw-record views beside the form
61
+ - **Section state persistence** — collapsed sections remembered across navigation
62
+ - **Escalation chains** — users can re-route work to other roles
63
+ - **AI triage** — optional auto-resolution for common patterns
64
+ - **Credential security** — password fields use ephemeral tokens, never stored in plain text
65
+ - **Telemetry** — trace IDs link escalations to OpenTelemetry traces
66
+ - **Bulk operations** — bulk claim, assign, escalate, triage, and cancel for queue management
67
+ - **Cancellation** — cancel pending escalations from the API or dashboard
201
68
 
202
- Inspect versions via `GET /api/roles/:role/schema?version=N`, `GET /api/roles/:role/schema/versions`, `lt.roles.getSchema` / `lt.roles.listSchemaVersions`, `ltc roles schema <role> --version N`, or the `get_role_schema` / `list_role_schema_versions` admin MCP tools. Save a new version by writing only the schema — `PATCH /api/roles/:role` with `form_schema` (+ optional `change_summary`), `lt.roles.update`, `ltc roles save-schema <role> --file schema.json`, or the `update_role` MCP tool. The dashboard edits it on the role's Escalation Schema page (`/admin/roles/:role/schema`), with the version history and snapshot viewer alongside.
69
+ You write the workflow and the schema. Everything else is provided.
203
70
 
204
71
  ---
205
72
 
206
- ## Recording the Outcome on Resolution
207
-
208
- An escalation row is created carrying **intent** — what was asked, who it routed to. Resolving it can stamp the **outcome** onto the same row: every resolve surface takes an optional `metadata` patch, merged into the row's GIN-indexed metadata.
209
-
210
- | Surface | How to pass it |
211
- |---------|----------------|
212
- | HTTP | `metadata` in the resolve body (`POST /api/escalations/:id/resolve`, `/resolve-by-signal-key`) |
213
- | SDK facade | `lt.escalations.resolve({ id, resolverPayload, metadata })` |
214
- | MCP | `metadata` arg on `claim_and_resolve` / `resolve_escalation` |
215
- | In-process library | `resolveEscalation(id, payload, metadata)` / `resolveEscalationBySignalKey(signalKey, payload, metadata)` |
216
-
217
- ```typescript
218
- await lt.escalations.resolve({
219
- id,
220
- resolverPayload: { approved: true }, // resumes the workflow; not indexed
221
- metadata: { outcome: 'approved', reviewedBy: 'alice', durationMs: elapsed },
222
- });
223
- ```
224
-
225
- The patch is distinct from the resolver payload: the payload resumes the paused workflow and is not indexed; the metadata patch is the durable, queryable record on the row. Use it for the audit trail and analytics — disposition, reviewer, time-to-resolve — so the escalation table answers *what was asked, what was decided, and how long it took* without a parallel log.
73
+ ## Section Map
226
74
 
227
- ### Resolving a set atomically
75
+ Ordered as a learning path — each file adds one capability to the same form:
228
76
 
229
- When one decision settles a SET of waits — each with its own payload — use
230
- `lt.escalations.resolveAllOrNone({ items })` (`POST /api/escalations/resolve-all-or-none`).
231
- Every listed row resolves with its own `resolverPayload` in one SQL statement,
232
- waking each parked workflow with its own value, or nothing resolves and the
233
- 409 body names exactly the rows that blocked (`failedIds` + reasons). Pass
234
- `requireClaimed: true` in claim-then-resolve flows to assert, inside the same
235
- statement, that every row is still assigned to the caller. See the
236
- [SDK reference](./api/sdk/escalations.md#resolveallornone) for the full contract.
77
+ | Topic | File |
78
+ |-------|------|
79
+ | Creating escalations with `conditionLT`, schema versioning | [escalation.md](hitl/escalation.md) |
80
+ | Field types, formats, required, read-only | [form.md](hitl/form.md) |
81
+ | Pre-submission validation guards (min, max, pattern, dynamic bounds) | [x-lt-validation.md](hitl/x-lt-validation.md) |
82
+ | Conditional visibility (`x-lt-showIf`, `x-lt-hide-if-empty`) | [x-lt-show-if.md](hitl/x-lt-show-if.md) |
83
+ | Layout, ordering, sections, binding, help panel | [x-lt-layout.md](hitl/x-lt-layout.md) |
84
+ | Custom widgets (checklist, file upload, code editor, signature, markdown) | [x-lt-widget.md](hitl/x-lt-widget.md) |
85
+ | List schema (`active-history`, `facet-table`) | [x-lt-list-schema.md](hitl/x-lt-list-schema.md) |
86
+ | Iframe viewport protocol | [iframe.md](hitl/iframe.md) |
87
+ | Claim lifecycle, resolving from system code, outcome recording, cancellation | [resolution.md](hitl/resolution.md) |
88
+ | Role routing, RBAC, scope, chains | [roles.md](hitl/roles.md) |
237
89
 
238
90
  ---
239
91
 
240
- ## JSON Schema Form Authoring
241
-
242
- The dashboard renders forms automatically from JSON Schema. No frontend code needed.
243
-
244
- The full custom vocabulary at a glance — every `x-lt-*` keyword and extension key the renderer honors:
92
+ ## Full Vocabulary Quick Reference
245
93
 
246
94
  | Keyword | Level | Purpose |
247
95
  |---------|-------|---------|
248
- | `x-lt-widget` | field | Rich control: `file-upload`, `code-editor`, `signature`, `rich-text`, `markdown` |
249
- | `x-lt-language` | field | Syntax hint shown by the `code-editor` widget |
250
- | `accept` | field | File-type filter for the `file-upload` widget (e.g. `".pdf,.png"`) |
251
- | `x-lt-bind` | field | Path this field's value occupies in the resolver payload (e.g. `"customer.email"`) |
96
+ | `x-lt-widget` | field | Rich control: `file-upload`, `code-editor`, `signature`, `rich-text`, `markdown`, `checklist` |
97
+ | `x-lt-source` | field | Data path for context-driven widgets: `"domain.path"` |
98
+ | `x-lt-language` | field | Syntax hint for the `code-editor` widget |
99
+ | `accept` | field | File-type filter for `file-upload` (e.g. `".pdf,.png"`) |
100
+ | `x-lt-bind` | field | Path in the resolver payload (e.g. `"customer.email"`) |
252
101
  | `x-lt-span` | field | Column span in a `two-column` layout (`2` = full width) |
253
- | `x-lt-showIf` | field | Show field only when a value exists at `domain.path`; prefix `!` to invert |
254
- | `x-lt-hide-if-empty` | field | `true` — suppress the field entirely when its value is null, `""`, `false`, or `0` |
255
- | `x-lt-section` | field | Label to group fields under (e.g. `"Facts"`, `"Station checks"`, `"Action"`) |
256
- | `x-lt-columns` | schema (list) | Column definitions for `facet-table` layout: `[{ label, value }]` |
102
+ | `x-lt-showIf` | field | Show field when a value is truthy at `domain.path`; prefix `!` to invert |
103
+ | `x-lt-hide-if-empty` | field | `true` — suppress the field when its value is null, `""`, `false`, or `0` |
104
+ | `x-lt-section` | field | Section group label |
105
+ | `x-lt-minimum` | field | Dynamic lower bound — resolves a `"domain.path"` from the escalation context |
106
+ | `x-lt-maximum` | field | Dynamic upper bound — resolves a `"domain.path"` from the escalation context |
107
+ | `x-lt-min-length` | field | Dynamic minimum string length — resolves a `"domain.path"` |
108
+ | `x-lt-max-length` | field | Dynamic maximum string length — resolves a `"domain.path"` |
109
+ | `x-lt-pattern-error` | field | Human-readable label for a `pattern` validation failure |
257
110
  | `x-lt-order` | schema | Field render sequence |
258
- | `x-lt-layout` | schema | `"two-column"` grid layout |
259
- | `x-lt-help` | schema | Markdown guidance for the side panel's Help view; `{{domain.path}}` tokens interpolate live record values |
111
+ | `x-lt-layout` | schema | `"two-column"` grid layout (form) or `"active-history"` / `"facet-table"` (list) |
112
+ | `x-lt-help` | schema | Markdown guidance for the side panel's Help view |
260
113
  | `x-lt-context` | schema | Plain-text fallback for the Help view when `x-lt-help` is absent |
261
114
  | `x-lt-viewport` | schema | Replace the generated form with a custom iframe UI |
115
+ | `x-lt-columns` | schema (list) | Column definitions for `facet-table` layout |
116
+ | `x-lt-active` | schema (list) | Active-item card definition |
117
+ | `x-lt-history` | schema (list) | History column definition |
262
118
  | `format` | field | Input specialization: `password`, `date`, `date-time`, `email`, `uri`, `textarea` |
263
- | `readOnly` | field | Static display (or a rendered content block with the `markdown` widget) |
264
- | `required` | schema | Fields that must be filled before submit |
265
- | `title` / `description` | both | Section header / helper text |
266
-
267
- The working reference is `examples/workflows/rich-form/` the `intake-reviewer` role's
268
- versioned `form_schema` (seeded by `examples/seed-rich-form.ts`) exercises the whole
269
- vocabulary in one form: side-panel help (`x-lt-help` with a checklist, table, tokens,
270
- and a relative link), two-column layout, ordering, date and email formats, enum, file
271
- upload, spans, required fields, and `x-lt-bind` mapping into a nested payload.
272
-
273
- ### Supported Field Types
274
-
275
- | JSON Type | Renders As |
276
- |-----------|-----------|
277
- | `boolean` | Checkbox toggle |
278
- | `number` | Number input |
279
- | `string` | Text input (default) |
280
- | `string` + `enum` | Dropdown select |
281
- | `null` | Read-only "null" display |
282
- | `array` | Tag display (read-only) |
283
- | `object` | Nested section with recursive fields |
284
-
285
- ### String Format Extensions
286
-
287
- Use the `format` keyword to get specialized inputs:
288
-
289
- | Format | Input Type |
290
- |--------|-----------|
291
- | `"password"` | Password field (masked, with ephemeral token redaction) |
292
- | `"date"` | Date picker |
293
- | `"date-time"` | Date + time picker |
294
- | `"email"` | Email input with validation |
295
- | `"uri"` | URL input |
296
- | `"textarea"` | Multi-line textarea (always, regardless of content length) |
297
-
298
- ```json
299
- {
300
- "properties": {
301
- "due_date": { "type": "string", "format": "date" },
302
- "contact_email": { "type": "string", "format": "email" },
303
- "detailed_notes": { "type": "string", "format": "textarea" }
304
- }
305
- }
306
- ```
307
-
308
- ### Custom Widgets (`x-lt-widget`)
309
-
310
- For rich inputs beyond standard HTML types:
311
-
312
- | Widget | Description |
313
- |--------|------------|
314
- | `"file-upload"` | File picker with drag-and-drop. Stores base64 data URL. Use `accept` to filter file types. |
315
- | `"code-editor"` | Monospace textarea with tab-key support. Use `x-lt-language` for syntax hint. |
316
- | `"signature"` | HTML5 Canvas drawing pad. Outputs PNG data URL. |
317
- | `"rich-text"` | Tall textarea for formatted text input. |
318
- | `"markdown"` | Markdown source, rendered with the same engine as the docs drawer (headings, tables, lists, code blocks, callouts). Editable fields get a Write/Preview toggle; with `readOnly: true` the field is a pure content block — see below. |
319
-
320
- ```json
321
- {
322
- "properties": {
323
- "screenshot": {
324
- "type": "string",
325
- "x-lt-widget": "file-upload",
326
- "accept": "image/*",
327
- "description": "Upload a screenshot of the issue"
328
- },
329
- "fix_script": {
330
- "type": "string",
331
- "x-lt-widget": "code-editor",
332
- "x-lt-language": "sql",
333
- "description": "SQL migration to apply"
334
- },
335
- "signature": {
336
- "type": "string",
337
- "x-lt-widget": "signature",
338
- "description": "Sign to confirm"
339
- }
340
- }
341
- }
342
- ```
343
-
344
- #### Markdown content blocks
345
-
346
- `readOnly: true` + `x-lt-widget: "markdown"` turns a field into a rendered content
347
- block: the markdown in its `default` displays as HTML inside the form — headings,
348
- tables, checklists, callouts. The versioned schema carries the page source itself,
349
- so review instructions and SOPs version with the form they belong to, and the
350
- source rides along in the resolver payload like any read-only field.
351
-
352
- ```json
353
- {
354
- "properties": {
355
- "review_guide": {
356
- "type": "string",
357
- "readOnly": true,
358
- "x-lt-widget": "markdown",
359
- "x-lt-span": 2,
360
- "default": "### Review checklist\n\n1. Confirm the **legal name** matches.\n2. Send a test message before approving.\n\n> Escalate non-standard contract language to legal."
361
- }
362
- }
363
- }
364
- ```
365
-
366
- Without `readOnly`, the field is a markdown *editor* — the resolver writes source in
367
- a Write/Preview toggle and the submitted value is the markdown text.
368
-
369
- ### Help Panel (`x-lt-help`)
370
-
371
- Schema-level `x-lt-help` carries the form's guidance — checklists, tier tables,
372
- callouts, links — as markdown. The dashboard renders it in the side panel beside the
373
- form, so the form itself stays a clean title and fields while the SOP sits one glance
374
- to the right. The help versions with the form: it lives in the same `form_schema`
375
- snapshot in `lt_role_schemas`.
376
-
377
- ```json
378
- {
379
- "title": "Customer Intake",
380
- "x-lt-help": "### Review checklist\n\n1. Confirm the **legal name** matches.\n2. Send a test message before approving.\n\nThis escalation is **{{escalation.status}}** in the **{{escalation.role}}** queue.\n\n[Back to the queue](/escalations/queue?role=intake-reviewer)",
381
- "properties": { ... }
382
- }
383
- ```
384
-
385
- `{{domain.path}}` tokens interpolate live values from the escalation surface using the
386
- `x-lt-bind` path syntax (dot keys, optional `[n]` indices). Five domains are available:
387
-
388
- | Domain | Resolves against |
389
- |--------|------------------|
390
- | `escalation` | The escalation row (`{{escalation.role}}`, `{{escalation.status}}`) |
391
- | `metadata` | The row's metadata dict (`{{metadata.schema_version}}`) |
392
- | `envelope` | The workflow-sent input envelope (`{{envelope.formDefaults.customer.name}}`) |
393
- | `payload` | The escalation context payload (`{{payload.category}}`) |
394
- | `resolver` | The submitted resolver payload (`{{resolver.notes}}`) |
395
-
396
- A missing value renders as an em dash. Links whose href starts with `/` navigate
397
- inside the dashboard.
398
-
399
- The Help view falls back in order: `x-lt-help` → `x-lt-context` (plain text) → a
400
- state-aware hint ("Claim this escalation to enable the form", "Fill out the form and
401
- submit to resolve it", and so on), so the panel always tells the resolver what the
402
- page expects of them. The panel's other views surface the record itself: **Metadata**
403
- (the row's metadata values), **Context** (input envelope, escalation context, resolver
404
- payload), and **Record** (the raw escalation JSON, builders only).
405
-
406
- ### Payload Binding (`x-lt-bind`)
407
-
408
- The form is flat; the payload the workflow consumes rarely is. A field may declare
409
- `x-lt-bind` — the path its value occupies in the resolver payload (dot keys, optional
410
- `[n]` indices). The dashboard maps the flat form through the binds on submit, and
411
- reverse-maps workflow-seeded `envelope.formDefaults` through them to prefill. A field
412
- with no bind lands at its own name at the payload root (1:1).
413
-
414
- ```json
415
- {
416
- "properties": {
417
- "customer_name": { "type": "string", "x-lt-bind": "customer.name" },
418
- "contact_email": { "type": "string", "format": "email", "x-lt-bind": "customer.email" },
419
- "tier": { "type": "string", "enum": ["starter", "professional"], "x-lt-bind": "contract.tier" },
420
- "notes": { "type": "string", "format": "textarea" }
421
- }
422
- }
423
- ```
424
-
425
- Submitting `{ customer_name, contact_email, tier, notes }` stores:
426
-
427
- ```json
428
- {
429
- "customer": { "name": "…", "email": "…" },
430
- "contract": { "tier": "…" },
431
- "notes": "…"
432
- }
433
- ```
434
-
435
- Only the FORM is versioned on the role — the payload shape is the workflow's own
436
- contract, produced by the binds. Evolve the form and its binds together, and the
437
- workflow's resolver type in the same commit.
438
-
439
- ### Layout Options (`x-lt-layout`)
440
-
441
- Control how fields are arranged:
442
-
443
- | Layout | Behavior |
444
- |--------|----------|
445
- | `"two-column"` | Fields in a 2-column grid. Use `x-lt-span: 2` on a field for full-width. |
446
-
447
- ```json
448
- {
449
- "x-lt-layout": "two-column",
450
- "properties": {
451
- "first_name": { "type": "string" },
452
- "last_name": { "type": "string" },
453
- "notes": { "type": "string", "format": "textarea", "x-lt-span": 2 }
454
- }
455
- }
456
- ```
457
-
458
- ### Field Ordering (`x-lt-order`)
459
-
460
- By default, fields render in JSON key order. Use `x-lt-order` to control sequence:
461
-
462
- ```json
463
- {
464
- "x-lt-order": ["priority", "decision", "notes"],
465
- "properties": {
466
- "notes": { "type": "string" },
467
- "decision": { "type": "string", "enum": ["approve", "reject", "defer"] },
468
- "priority": { "type": "number" }
469
- }
470
- }
471
- ```
472
-
473
- ### Conditional Visibility (`x-lt-showIf`)
474
-
475
- A field can be hidden or shown based on a value present in the escalation record. Use `x-lt-showIf` on any property to make it conditional:
476
-
477
- ```json
478
- "x-lt-showIf": "domain.path"
479
- ```
480
-
481
- The value at `domain.path` is evaluated for truthiness. If it is present and truthy the field shows; if absent, null, false, or an empty string it is hidden. Prefix `!` to invert: show when the value is absent.
482
-
483
- Domains follow the same `domain.path` convention as `x-lt-help` tokens:
484
-
485
- | Domain | Resolves against |
486
- |--------|-----------------|
487
- | `metadata` | The row's metadata dict |
488
- | `payload` | The escalation context payload (`escalation_payload`) |
489
- | `envelope` | The workflow-sent input envelope |
490
- | `escalation` | Top-level escalation row fields (`role`, `status`, `priority`, …) |
491
- | `resolver` | The submitted resolver payload |
492
-
493
- **Example — item type branching:**
494
-
495
- A role where the queue receives both regular work items and crew-pill shutdown signals. The payload carries `item_type` to distinguish them.
496
-
497
- ```json
498
- {
499
- "title": "Worker Station",
500
- "properties": {
501
- "action_taken": {
502
- "type": "string",
503
- "enum": ["completed", "deferred", "escalated"],
504
- "description": "Outcome for this work item",
505
- "x-lt-showIf": "!payload.crew_pill"
506
- },
507
- "notes": {
508
- "type": "string",
509
- "format": "textarea",
510
- "x-lt-showIf": "!payload.crew_pill"
511
- },
512
- "shutdown_ack": {
513
- "type": "boolean",
514
- "title": "Acknowledge shutdown",
515
- "description": "Confirm you are stopping work and clearing the station",
516
- "x-lt-showIf": "payload.crew_pill"
517
- }
518
- }
519
- }
520
- ```
521
-
522
- When `escalation_payload` contains `{ "crew_pill": true }`, only `shutdown_ack` renders. When the payload carries a regular item (no `crew_pill` key), only `action_taken` and `notes` render.
523
-
524
- `x-lt-showIf` is evaluated against the escalation record and the **live form state** (via the `resolver` domain). Conditions based on `metadata`, `payload`, `envelope`, and `escalation` are static (from the stored row); conditions based on `resolver` react in real time as the user edits the form. Hidden fields are not rendered but their values (if any) remain in form state and are submitted only if they were filled before being hidden.
525
-
526
- **Example — approval/rejection conditional fields:**
527
-
528
- The employee sets `approved`. When they uncheck it, `rejection_reason` and `rejection_notes` appear immediately — no page reload, no submit required. The workflow receives the full payload and branches on `approved`:
529
-
530
- ```json
531
- {
532
- "title": "Rejection Review",
533
- "x-lt-layout": "two-column",
534
- "x-lt-order": ["order_id", "rejection_type", "approved", "rejection_reason", "rejection_notes"],
535
- "required": ["approved"],
536
- "properties": {
537
- "order_id": { "type": "string", "readOnly": true, "x-lt-section": "The Report" },
538
- "rejection_type": { "type": "string", "readOnly": true, "x-lt-section": "The Report" },
539
- "approved": { "type": "boolean", "x-lt-section": "The Verdict" },
540
- "rejection_reason": {
541
- "type": "string",
542
- "enum": ["Quality", "Quantity", "Routing", "Other"],
543
- "x-lt-showIf": "!resolver.approved",
544
- "x-lt-section": "The Verdict"
545
- },
546
- "rejection_notes": {
547
- "type": "string",
548
- "format": "textarea",
549
- "x-lt-span": 2,
550
- "x-lt-showIf": "!resolver.approved",
551
- "x-lt-section": "The Verdict"
552
- }
553
- }
554
- }
555
- ```
556
-
557
- In the workflow, branch on `approved` from the resolver payload:
558
-
559
- ```typescript
560
- const decision = await conditionLT<{
561
- approved: boolean;
562
- rejection_reason?: string;
563
- rejection_notes?: string;
564
- }>(signalId, { role: 'quality-reviewer', /* ... */ });
565
-
566
- if (!decision.approved) {
567
- await sendForRework({ reason: decision.rejection_reason, notes: decision.rejection_notes });
568
- } else {
569
- await advanceOrder();
570
- }
571
- ```
572
-
573
- ### Suppressing Empty Fields (`x-lt-hide-if-empty`)
574
-
575
- A field with `"x-lt-hide-if-empty": true` is hidden when its value is null, an empty string, `false`, or `0`. This is primarily useful for `readOnly` fact fields that are only present on some records:
576
-
577
- ```json
578
- {
579
- "properties": {
580
- "heel_raise": {
581
- "type": "string",
582
- "readOnly": true,
583
- "x-lt-hide-if-empty": true,
584
- "description": "Heel raise specification — hidden when not present on this order"
585
- }
586
- }
587
- }
588
- ```
589
-
590
- The field remains in form state and is included in the submitted payload if it has a value. Only the visual row is suppressed.
591
-
592
- ### Section Headers (`x-lt-section`)
593
-
594
- Group related fields under a labeled section by adding `"x-lt-section": "Label"` to each property. Fields that share the same consecutive section name are collected into one group with a header:
595
-
596
- ```json
597
- {
598
- "x-lt-layout": "two-column",
599
- "x-lt-order": ["patient_id", "heel_cup", "pdac", "approved", "notes"],
600
- "properties": {
601
- "patient_id": { "type": "string", "readOnly": true, "x-lt-section": "Facts", "x-lt-hide-if-empty": true },
602
- "heel_cup": { "type": "string", "readOnly": true, "x-lt-section": "Facts", "x-lt-hide-if-empty": true },
603
- "pdac": { "type": "boolean", "readOnly": true, "x-lt-section": "Facts", "x-lt-hide-if-empty": true },
604
- "approved": { "type": "string", "enum": ["yes", "no"], "x-lt-section": "Action" },
605
- "notes": { "type": "string", "format": "textarea", "x-lt-span": 2, "x-lt-section": "Action" }
606
- }
607
- }
608
- ```
609
-
610
- Sections are ordered by the first field that carries the section name (respects `x-lt-order`). A field without `x-lt-section` belongs to an unnamed group rendered without a header. Named sections render with a left accent line that spans the full section height, a small icon, and the label in uppercase — spatially grouping the header and its fields without a card or border box.
611
-
612
- ### Validation (`required`)
613
-
614
- Fields listed in `required` show a red asterisk and block submission when empty:
615
-
616
- ```json
617
- {
618
- "required": ["decision"],
619
- "properties": {
620
- "decision": { "type": "string", "enum": ["approve", "reject"] },
621
- "notes": { "type": "string", "description": "Optional comments" }
622
- }
623
- }
624
- ```
625
-
626
- ### Read-Only Fields (`readOnly`)
627
-
628
- Fields with `readOnly: true` display as static text. Useful for showing context alongside editable fields:
629
-
630
- ```json
631
- {
632
- "properties": {
633
- "request_amount": { "type": "number", "readOnly": true },
634
- "approved_amount": { "type": "number", "description": "Enter the approved amount" }
635
- }
636
- }
637
- ```
638
-
639
- ### Schema Title and Description
640
-
641
- The `title` and `description` at the schema root are used in the UI:
642
- - **`title`**: Shown as the form's section header
643
- - **`description`**: Shown as helper text beneath the title — keep it to a short phrase; longer guidance belongs in `x-lt-help`
644
- - **`x-lt-help`**: Rendered as markdown in the side panel beside the form
645
-
646
- ```json
647
- {
648
- "title": "Expense Approval",
649
- "description": "Review the expense report below. Verify receipts match the claimed amounts. Approve or reject with notes.",
650
- "properties": { ... }
651
- }
652
- ```
653
-
654
- ---
655
-
656
- ## Iframe Viewport Protocol
657
-
658
- For fully custom UIs (PDF viewers, complex multi-step forms, specialized domain UIs), use an iframe viewport.
659
-
660
- ### Schema Declaration
661
-
662
- ```json
663
- {
664
- "x-lt-viewport": {
665
- "type": "iframe",
666
- "src": "https://your-app.example.com/hitl-form"
667
- },
668
- "properties": { ... }
669
- }
670
- ```
671
-
672
- When `x-lt-viewport` is present, the dashboard renders an iframe instead of the standard form.
673
-
674
- ### Message Protocol
675
-
676
- Communication happens via `window.postMessage`.
677
-
678
- #### Parent to Iframe
679
-
680
- ```typescript
681
- // Sent when the iframe signals ready or on load
682
- {
683
- type: 'lt:init',
684
- escalation: {
685
- id: string,
686
- type: string,
687
- subtype: string,
688
- description: string | null,
689
- status: string,
690
- priority: number,
691
- role: string,
692
- workflow_type: string | null,
693
- },
694
- schema: Record<string, unknown>, // The full form schema
695
- }
696
-
697
- // Optional: parent requests the iframe to submit
698
- {
699
- type: 'lt:requestSubmit'
700
- }
701
- ```
702
-
703
- #### Iframe to Parent
704
-
705
- ```typescript
706
- // Signal that the iframe is ready to receive init data
707
- { type: 'lt:ready' }
708
-
709
- // Submit the human's response — triggers escalation resolution
710
- { type: 'lt:submit', payload: { approved: true, notes: '...' } }
711
-
712
- // Escalate to a different role
713
- { type: 'lt:escalate', target: 'senior-reviewer' }
714
-
715
- // Auto-resize the iframe
716
- { type: 'lt:resize', height: 600 }
717
- ```
718
-
719
- ### Minimal Example
720
-
721
- ```html
722
- <!DOCTYPE html>
723
- <html>
724
- <head><title>Custom HITL Form</title></head>
725
- <body>
726
- <div id="form"></div>
727
- <button id="submit">Approve</button>
728
-
729
- <script>
730
- // Signal ready
731
- window.parent.postMessage({ type: 'lt:ready' }, '*');
732
-
733
- // Receive init data
734
- window.addEventListener('message', (event) => {
735
- if (event.data.type === 'lt:init') {
736
- const { escalation, schema } = event.data;
737
- document.getElementById('form').textContent =
738
- `Reviewing: ${escalation.description}`;
739
- }
740
- });
741
-
742
- // Submit response
743
- document.getElementById('submit').addEventListener('click', () => {
744
- window.parent.postMessage({
745
- type: 'lt:submit',
746
- payload: { approved: true, reviewed_at: new Date().toISOString() },
747
- }, '*');
748
- });
749
- </script>
750
- </body>
751
- </html>
752
- ```
753
-
754
- ### Security
755
-
756
- - The iframe runs with `sandbox="allow-scripts allow-same-origin allow-forms"`
757
- - The parent validates message origins — only messages from the iframe's origin are accepted
758
- - The `envelope` field (which may contain secrets) is NOT sent to the iframe
759
- - Only safe escalation metadata (id, type, description, status, priority, role) is exposed
760
-
761
- ---
762
-
763
- ## The Escalation Detail Page
764
-
765
- The escalation detail page has one view, built for the person resolving the item: the
766
- escalation's description is the page title, the form starts directly beneath it, and
767
- the action bar closes the page. The lifecycle sparkline (waiting / claimed / resolved
768
- ratios) sits as a short persistent row above the side panel. Everything else lives in
769
- the side panel, ordered by specificity:
770
-
771
- | View | Shows | Available to |
772
- |------|-------|--------------|
773
- | **Help** | The form's `x-lt-help` markdown, or a state-aware hint | Everyone |
774
- | **Details** | Status, role, priority, claim provenance, timestamps; identifier links below a divider | Everyone (identifiers: builders) |
775
- | **AI Analysis** | What triage diagnosed and corrected | When AI is enabled and triage data is present |
776
- | **Metadata** | The row's metadata values | Everyone |
777
- | **Context** | Input envelope, escalation context, resolver payload | Everyone |
778
- | **Record** | The raw escalation JSON | Builders (admins, superadmins, engineers) |
779
-
780
- The page is two fixed-height columns beneath the global toolbar — the form column and
781
- the panel each scroll independently, so the panel stays pinned like the left nav while
782
- long forms or long panel content scroll. The form column narrows as the panel expands
783
- (the panel is capped at half the page). When the form carries `x-lt-help`, the panel
784
- opens expanded on the Help view; otherwise it stays hidden until the page-header panel
785
- button summons it.
786
-
787
- ### Designing the Form
788
-
789
- To create a polished resolve experience:
790
-
791
- 1. Set `title` on your schema — it replaces the section header
792
- 2. Set `x-lt-help` — checklists, tables, and links render as markdown in the side panel, with `{{domain.path}}` tokens for live record values; keep `description` to a short subtitle
793
- 3. Use `readOnly` fields for context the human needs to see but shouldn't edit
794
- 4. Use `x-lt-order` to put the most important fields first
795
- 5. Use `required` to guide users on what must be filled
796
- 6. Use descriptive `description` on individual fields for inline help text
797
-
798
- ---
799
-
800
- ## Escalations List Schema
801
-
802
- The form schema formats one escalation on the detail page. A role can also own a
803
- `list_schema` that formats its whole **list** page — the list-page analog of the
804
- resolve form. It is opt-in and applies only when the list is scoped to exactly one
805
- role (`/escalations/available?role=<role>`). Absent, the list renders the standard
806
- engineer table; present, a rich role-authored view renders with a "Table view" toggle
807
- one click away. It is versioned **independently** of the form schema (its own timeline;
808
- a list edit never bumps the form version) and edited on its own page,
809
- `/admin/roles/:role/list-schema`. The list always renders the latest version.
810
-
811
- This is what turns a queue like a `policy-document` role — where a looped workflow
812
- keeps exactly one escalation live and each resolved one is a revision — into a document
813
- with a history, instead of a one-row table.
814
-
815
- ### Vocabulary
816
-
817
- Every string is a markdown/text template run through the same `{{domain.path}}` token
818
- binding as `x-lt-help` (domains `escalation | metadata | envelope | payload | resolver`,
819
- evaluated against each row); `body` strings render through the markdown renderer.
820
-
821
- | Key | Level | Purpose |
822
- |-----|-------|---------|
823
- | `x-lt-layout` | schema | `"active-history"` (two columns), `"active"` (card only), `"facet-table"` (multi-row queue), or `"table"` (fallback) |
824
- | `x-lt-help` | schema | Optional markdown header, interpolated with the active row |
825
- | `x-lt-active` | schema | The live item card: `{ title, subtitle?, body?, fields?: [{label, value}] }` |
826
- | `x-lt-history` | schema | History column: `{ row: { title, subtitle?, meta? }, limit?, status? }` |
827
- | `x-lt-columns` | schema | Column definitions for `facet-table` layout: `[{ label: string, value: string }]` — `value` is a `{{domain.path}}` token |
828
-
829
- The **active** item is the first non-terminal escalation. The **history** column is not
830
- auto-loaded — a "Load full history" link fetches resolved items on demand (`status`
831
- defaults to `resolved`, `limit` to 25). Unknown/absent `x-lt-layout` is a safe no-op
832
- that falls back to the table.
833
-
834
- ### Example — a policy-document role
835
-
836
- ```json
837
- {
838
- "x-lt-layout": "active-history",
839
- "x-lt-help": "# {{metadata.title}}\nThe authoritative policy. One revision is live at a time.",
840
- "x-lt-active": {
841
- "title": "{{metadata.title}}",
842
- "subtitle": "Revision {{metadata.revision}} · effective {{metadata.effective_date}}",
843
- "body": "{{metadata.document_markdown}}",
844
- "fields": [
845
- { "label": "Owner", "value": "{{metadata.owner}}" },
846
- { "label": "Claimed by", "value": "{{escalation.assigned_to}}" }
847
- ]
848
- },
849
- "x-lt-history": {
850
- "row": { "title": "{{metadata.title}} — revision {{metadata.revision}}" },
851
- "limit": 25
852
- }
853
- }
854
- ```
855
-
856
- The working reference is `examples/workflows/policy-document/` (role seeded by
857
- `examples/seed-policy-document.ts`): a looped workflow opens one policy-review
858
- escalation, parks on it, and folds each resolution into the next revision — so the
859
- policy facts ride the row's metadata and the list view reads them with `{{metadata.*}}`.
860
-
861
- ### `facet-table` layout — queue as a column table
862
-
863
- Use `"x-lt-layout": "facet-table"` when the queue contains many concurrent rows and the
864
- role's context is best expressed as a scannable table — a print farm, order queue, or
865
- batch-processing pond. Every pending escalation is a row; columns are defined by
866
- `x-lt-columns`.
867
-
868
- ```json
869
- {
870
- "x-lt-layout": "facet-table",
871
- "x-lt-columns": [
872
- { "label": "Patient", "value": "{{metadata.patientId}}" },
873
- { "label": "Heel cup", "value": "{{metadata.heelCup}}" },
874
- { "label": "PDAC", "value": "{{metadata.pdac}}" },
875
- { "label": "Station", "value": "{{metadata.station}}" },
876
- { "label": "Priority", "value": "{{escalation.priority}}" },
877
- { "label": "Created", "value": "{{escalation.created_at}}" }
878
- ]
879
- }
880
- ```
881
-
882
- A status dot precedes the first column automatically. ISO datetime values
883
- (`{{escalation.created_at}}`, `{{escalation.resolved_at}}`, etc.) render as a
884
- readable relative date with a full-timestamp tooltip. Missing/unresolvable token values
885
- render as an em dash. Clicking any row navigates to the detail page. `x-lt-help` and
886
- `x-lt-active` are ignored in this layout.
887
-
888
- ---
889
-
890
- ## Role-Based Routing
891
-
892
- Escalations are routed by role. Users only see escalations for roles they hold.
893
-
894
- ```typescript
895
- // Workflow escalates to a specific role
896
- await ltCreateEscalation({
897
- role: 'finance-reviewer', // Only users with this role see it
898
- // ...
899
- });
900
- ```
901
-
902
- ### Work-Surface Scope
903
-
904
- A `member` of a role carries a work-surface scope that narrows what they see and act on within that queue: `read_scope` (`self` | `all`) governs which escalations they see, and `write_scope` (`none` | `self` | `all`) governs which they can claim, resolve, or cancel. `self` means escalations assigned to that member (`assigned_to = user`); `all` means the whole role queue. `admin` and `superadmin` always work the whole queue. See the [Roles API](api/http/roles.md#work-surface-scope) for the five member profiles and the assignment contract.
905
-
906
- Scope is a property of the **membership** (`lt_user_roles`), not of the escalation. The escalation engine is unchanged: `condition()` / `conditionLT()`, `ltCreateEscalation`, and the interceptor write escalation rows (`hmsh_escalations`) exactly as before — an escalation carries a `role` and an optional `assigned_to`, with no scope column. Scope is resolved at read time, when a *user* lists or acts on the queue. Escalating to a role that does not exist yet still registers the role name only — roles are typeless; `type` and scope are set per user when the role is granted. The one place scope affects creation is the standalone `POST /api/escalations` HTTP endpoint, which requires `write_scope=all`; workflow-emitted escalations (`conditionLT`, interceptor returns, `ltCreateEscalation`) do not pass through that check.
907
-
908
- ### One-Time and Pre-Assigned Users
909
-
910
- To route a single item to a named person, assign the escalation to them and provision them with `read_scope=self` + `write_scope=self`. The workflow sets `assigned_to` to the person's user ID (a pre-claim) when it creates the escalation, then provisions or updates that user as a `member` with self/self scope on the target role. They land directly on that one item — a just-in-time form scoped by RBAC, with no access to the rest of the queue and no direct table access. An update or follow-up is simply another workflow firing another escalation to that same person.
911
-
912
- ```typescript
913
- // Pre-assign the escalation to a specific person and route a one-time form to them
914
- await ltCreateEscalation({
915
- role: 'customer-triage',
916
- assigned_to: userId, // pre-claim — durable, keyed off the user, not the soft-lock TTL
917
- description: 'Confirm your shipping address',
918
- metadata: {
919
- form_schema: {
920
- title: 'Confirm Address',
921
- properties: { address: { type: 'string' }, confirmed: { type: 'boolean' } },
922
- required: ['confirmed'],
923
- },
924
- },
925
- });
926
- // The person is provisioned as a member of `customer-triage` with read_scope=self, write_scope=self.
927
- ```
928
-
929
- ### Escalation Chains
930
-
931
- Users can escalate to other roles via the "Escalate" tab:
932
-
933
- ```
934
- Analyst → Senior Analyst → Manager → VP
935
- ```
936
-
937
- Configure escalation targets in the role configuration (Admin > Roles). Each role defines which other roles it can escalate to.
938
-
939
- ### Multi-Tier Example
940
-
941
- ```typescript
942
- // Level 1: Auto-review
943
- const result = await autoReview(document);
944
-
945
- if (result.confidence < 0.8) {
946
- // Level 2: Human analyst
947
- await ltCreateEscalation({
948
- role: 'analyst',
949
- description: `Low confidence review (${result.confidence})`,
950
- metadata: {
951
- form_schema: {
952
- title: 'Document Review',
953
- properties: {
954
- approved: { type: 'boolean' },
955
- corrections: { type: 'string', format: 'textarea' },
956
- },
957
- required: ['approved'],
958
- },
959
- },
960
- });
961
- // User can further escalate to 'senior-analyst' or 'manager' from the dashboard
962
- }
963
- ```
964
-
965
- ---
966
-
967
- ## Worked Examples
968
-
969
- ### Simple Approval
970
-
971
- A workflow needs a yes/no decision with optional notes.
972
-
973
- ```typescript
974
- metadata: {
975
- signal_id: signalId,
976
- form_schema: {
977
- title: 'Approve Request',
978
- description: 'Review the details and approve or reject this request.',
979
- required: ['approved'],
980
- properties: {
981
- approved: { type: 'boolean', description: 'Check to approve' },
982
- notes: { type: 'string', format: 'textarea', description: 'Optional comments' },
983
- },
984
- },
985
- }
986
- ```
987
-
988
- ### Document Review with PDF Viewer
989
-
990
- Use an iframe viewport to embed a PDF viewer alongside approval controls.
991
-
992
- ```typescript
993
- metadata: {
994
- signal_id: signalId,
995
- form_schema: {
996
- title: 'Document Review',
997
- 'x-lt-viewport': {
998
- type: 'iframe',
999
- src: 'https://internal.example.com/pdf-reviewer',
1000
- },
1001
- },
1002
- }
1003
- ```
1004
-
1005
- The iframe at `pdf-reviewer` loads the document, renders it with a viewer, and posts `lt:submit` with the review decision.
1006
-
1007
- ### Multi-Field Data Entry
1008
-
1009
- A complex form with layout and validation.
1010
-
1011
- ```typescript
1012
- metadata: {
1013
- signal_id: signalId,
1014
- form_schema: {
1015
- title: 'Customer Intake',
1016
- description: 'Complete the customer information form. All required fields must be filled before submission.',
1017
- 'x-lt-layout': 'two-column',
1018
- 'x-lt-order': ['first_name', 'last_name', 'email', 'phone', 'tier', 'notes'],
1019
- required: ['first_name', 'last_name', 'email', 'tier'],
1020
- properties: {
1021
- first_name: { type: 'string' },
1022
- last_name: { type: 'string' },
1023
- email: { type: 'string', format: 'email' },
1024
- phone: { type: 'string' },
1025
- tier: {
1026
- type: 'string',
1027
- enum: ['free', 'pro', 'enterprise'],
1028
- description: 'Select the customer tier',
1029
- },
1030
- notes: {
1031
- type: 'string',
1032
- format: 'textarea',
1033
- 'x-lt-span': 2,
1034
- description: 'Additional notes about this customer',
1035
- },
1036
- },
1037
- },
1038
- }
1039
- ```
1040
-
1041
- ### Credential Provisioning
1042
-
1043
- Password fields are automatically redacted and replaced with ephemeral tokens (15-min TTL) before being sent back to the workflow.
1044
-
1045
- ```typescript
1046
- metadata: {
1047
- signal_id: signalId,
1048
- form_schema: {
1049
- title: 'Provide Credentials',
1050
- description: 'Enter the API credentials for this integration. Passwords are encrypted and stored as ephemeral tokens.',
1051
- required: ['api_key', 'api_secret'],
1052
- properties: {
1053
- api_key: { type: 'string', description: 'API Key' },
1054
- api_secret: { type: 'string', format: 'password', description: 'API Secret (will be redacted)' },
1055
- environment: {
1056
- type: 'string',
1057
- enum: ['sandbox', 'production'],
1058
- description: 'Target environment',
1059
- },
1060
- },
1061
- },
1062
- }
1063
- ```
1064
-
1065
- ---
1066
-
1067
- ## Resolving from System Code
1068
-
1069
- When a backend service (not the dashboard UI) needs to resolve an escalation — for example, an ingress handler that receives a webhook or processes a domain event — use the escalation SDK methods directly.
1070
-
1071
- ### By escalation ID
1072
-
1073
- Use when you already have the escalation UUID (e.g. stored in your own DB alongside the order):
1074
-
1075
- ```typescript
1076
- const result = await lt.escalations.resolve({
1077
- id: escalationId,
1078
- resolverPayload: { approved: true, targetStatus: 'ready' },
1079
- });
1080
- ```
1081
-
1082
- This routes through the full resolution path and works for all escalation types — atomic `conditionLT` (signal_key), legacy `conditionLT` (signal_id), and re-run-style escalations.
1083
-
1084
- ### By metadata key-value pair
1085
-
1086
- Use when you know a domain identifier (e.g. `orderId`) but not the escalation UUID. `resolveByMetadata` finds the highest-priority pending escalation matching the key-value pair and resolves it atomically — no pre-flight lookup, no TOCTOU:
1087
-
1088
- ```typescript
1089
- const result = await lt.escalations.resolveByMetadata({
1090
- key: 'orderId',
1091
- value: orderId,
1092
- resolverPayload: { approved: true, targetStatus: 'ready' },
1093
- });
1094
-
1095
- if (result.status === 404) {
1096
- // No pending escalation for this orderId
1097
- }
1098
- ```
1099
-
1100
- This works for all escalation types including atomic `conditionLT` rows (those with `signal_key` set). The routing is transparent — the caller does not need to know which pattern the workflow used.
1101
-
1102
- ### By signal key
1103
-
1104
- When the signal key is deterministic and known to the caller (e.g. `station-done-${workflowId}`), use the direct signal-key path to skip the metadata lookup:
1105
-
1106
- ```typescript
1107
- await lt.escalations.resolveBySignalKey({
1108
- signalKey: `station-done-${workflowId}`,
1109
- resolverPayload: { approved: true },
1110
- });
1111
- ```
1112
-
1113
- ---
1114
-
1115
- ## Cancelling Escalations
1116
-
1117
- Escalations can be cancelled at any point before they are resolved. Cancellation is terminal — a cancelled escalation cannot be re-opened.
1118
-
1119
- ### When cancellation happens
1120
-
1121
- - **Workflow termination** — when you terminate a workflow (`POST /api/workflows/:workflowId/terminate`), HotMesh automatically cancels any pending escalations tied to it. The waiting `conditionLT` call returns `null`.
1122
- - **Explicit cancel** — cancel a single escalation via the API or from the dashboard. Any workflow waiting on that escalation via `conditionLT` receives `null`.
1123
-
1124
- ### API
1125
-
1126
- ```
1127
- POST /api/escalations/:id/cancel # single escalation
1128
- POST /api/escalations/bulk-cancel # { "ids": [...] }
1129
- ```
1130
-
1131
- Returns 409 if the escalation is already resolved or cancelled.
1132
-
1133
- ### Dashboard
1134
-
1135
- - **Available escalations list** — select one or more rows and click **Cancel** in the bulk action bar. A confirmation modal appears before any action is taken.
1136
- - **Escalation detail page** — a Cancel link appears in the action bar when the escalation is in `available` or `claimed_by_me` state. Terminal escalations (resolved or cancelled) show no cancel affordance.
1137
-
1138
- ### Handling cancellation in workflows
1139
-
1140
- `conditionLT` returns `T | false | null`. Always guard before accessing the payload:
1141
-
1142
- ```typescript
1143
- const decision = await conditionLT<{ approved: boolean }>(signalId, escalationConfig);
1144
-
1145
- if (decision === null) {
1146
- // Escalation was cancelled (workflow terminated or explicit cancel)
1147
- return { type: 'return' as const, data: { cancelled: true } };
1148
- }
1149
- if (decision === false) {
1150
- // Escalation timed out
1151
- return { type: 'return' as const, data: { timedOut: true } };
1152
- }
1153
-
1154
- // Normal path — decision is the resolver's payload
1155
- ```
1156
-
1157
- The `!decision` shorthand handles both cases when you don't need to distinguish between them:
1158
-
1159
- ```typescript
1160
- if (!decision) {
1161
- return { type: 'return' as const, data: { cancelled: true } };
1162
- }
1163
- ```
1164
-
1165
- ---
1166
-
1167
- ## What Long-tail Provides (For Free)
1168
-
1169
- When you author a HITL-backed workflow, the platform handles:
1170
-
1171
- - **Escalation routing** — role-based, priority-ordered work queues
1172
- - **Claim/release** — soft-lock with TTL, prevents duplicate work
1173
- - **Real-time updates** — NATS/Socket.IO events push changes to the dashboard instantly
1174
- - **Form rendering** — JSON Schema to rich form controls, no frontend code needed
1175
- - **Side panel** — help, AI analysis, metadata, context, and raw-record views beside the form
1176
- - **Section state persistence** — collapsed sections remembered across navigation
1177
- - **Escalation chains** — users can re-route work to other roles
1178
- - **AI triage** — optional auto-resolution for common patterns
1179
- - **Signal routing** — 5 resolution paths (conditionLT, waitFor, triage, re-run, notification-only)
1180
- - **Credential security** — password fields use ephemeral tokens, never stored in plain text
1181
- - **Telemetry** — trace IDs link escalations to OpenTelemetry traces
1182
- - **Bulk operations** — bulk claim, assign, escalate, triage, and cancel for queue management
1183
- - **Cancellation** — cancel pending escalations from the API or dashboard; `conditionLT` returns `null` so workflows handle it cleanly
1184
-
1185
- You write the workflow and the schema. Everything else is provided.
119
+ | `readOnly` | field | Static display |
120
+ | `required` | schema | Fields that block submission when empty |
121
+ | `title` / `description` | both | Form section header / helper text |
122
+ | `minimum` / `maximum` | field | Static numeric bounds — enforced before submission |
123
+ | `exclusiveMinimum` / `exclusiveMaximum` | field | Exclusive numeric bounds |
124
+ | `minLength` / `maxLength` | field | Static string length bounds |
125
+ | `pattern` | field | Regexp guard enforced before submission |