@hotmeshio/long-tail 0.9.6 → 0.10.1

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-DXFIKXO3.js → AdminDashboard-D7Amh-EJ.js} +2 -2
  14. package/dashboard/dist/assets/{AdminDashboard-DXFIKXO3.js.map → AdminDashboard-D7Amh-EJ.js.map} +1 -1
  15. package/dashboard/dist/assets/{AgentConfigPage-CQE1mfjk.js → AgentConfigPage-CW2udbSO.js} +7 -7
  16. package/dashboard/dist/assets/{AgentConfigPage-CQE1mfjk.js.map → AgentConfigPage-CW2udbSO.js.map} +1 -1
  17. package/dashboard/dist/assets/{AgentDetailPage-DM59ozP_.js → AgentDetailPage-Dy3Qvlfk.js} +3 -3
  18. package/dashboard/dist/assets/{AgentDetailPage-DM59ozP_.js.map → AgentDetailPage-Dy3Qvlfk.js.map} +1 -1
  19. package/dashboard/dist/assets/{AgentsPage-DSK2uXI5.js → AgentsPage-D7WePZ0o.js} +2 -2
  20. package/dashboard/dist/assets/{AgentsPage-DSK2uXI5.js.map → AgentsPage-D7WePZ0o.js.map} +1 -1
  21. package/dashboard/dist/assets/AvailableEscalationsPage-Dx51E6s_.js +2 -0
  22. package/dashboard/dist/assets/AvailableEscalationsPage-Dx51E6s_.js.map +1 -0
  23. package/dashboard/dist/assets/BotPicker-KebTxTDi.js +2 -0
  24. package/dashboard/dist/assets/BotPicker-KebTxTDi.js.map +1 -0
  25. package/dashboard/dist/assets/CapabilitiesPage-3VTLEPGO.js +2 -0
  26. package/dashboard/dist/assets/{CapabilitiesPage-DFQryGBj.js.map → CapabilitiesPage-3VTLEPGO.js.map} +1 -1
  27. package/dashboard/dist/assets/{CollapsibleSection-B7-d5O_x.js → CollapsibleSection-BYgeZz9M.js} +2 -2
  28. package/dashboard/dist/assets/{CollapsibleSection-B7-d5O_x.js.map → CollapsibleSection-BYgeZz9M.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-DFqEYmiF.js +2 -0
  32. package/dashboard/dist/assets/CountdownTimer-DFqEYmiF.js.map +1 -0
  33. package/dashboard/dist/assets/CredentialsPage-CaH3J3SE.js +2 -0
  34. package/dashboard/dist/assets/{CredentialsPage-BWVgxRpZ.js.map → CredentialsPage-CaH3J3SE.js.map} +1 -1
  35. package/dashboard/dist/assets/{CronLabel-DNnyNqXH.js → CronLabel-Bma_4AC2.js} +2 -2
  36. package/dashboard/dist/assets/{CronLabel-DNnyNqXH.js.map → CronLabel-Bma_4AC2.js.map} +1 -1
  37. package/dashboard/dist/assets/{CustomDurationPicker-SNsQZL_U.js → CustomDurationPicker-BxyjEX27.js} +2 -2
  38. package/dashboard/dist/assets/{CustomDurationPicker-SNsQZL_U.js.map → CustomDurationPicker-BxyjEX27.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-C4r0IvUt.js → ElapsedCell-Dr1cEwoi.js} +2 -2
  42. package/dashboard/dist/assets/{ElapsedCell-C4r0IvUt.js.map → ElapsedCell-Dr1cEwoi.js.map} +1 -1
  43. package/dashboard/dist/assets/{EscalationListSchemaPage-D8Ec8Zhj.js → EscalationListSchemaPage-CLdsR-hK.js} +3 -3
  44. package/dashboard/dist/assets/{EscalationListSchemaPage-D8Ec8Zhj.js.map → EscalationListSchemaPage-CLdsR-hK.js.map} +1 -1
  45. package/dashboard/dist/assets/{EscalationSchemaPage-BmEv7qU2.js → EscalationSchemaPage-DnEnvolM.js} +3 -3
  46. package/dashboard/dist/assets/{EscalationSchemaPage-BmEv7qU2.js.map → EscalationSchemaPage-DnEnvolM.js.map} +1 -1
  47. package/dashboard/dist/assets/{EscalationsOverview-DkIy025e.js → EscalationsOverview-BH8cxgLI.js} +2 -2
  48. package/dashboard/dist/assets/{EscalationsOverview-DkIy025e.js.map → EscalationsOverview-BH8cxgLI.js.map} +1 -1
  49. package/dashboard/dist/assets/{EventTable-Co4RYeKO.js → EventTable-p-xsfYYg.js} +2 -2
  50. package/dashboard/dist/assets/{EventTable-Co4RYeKO.js.map → EventTable-p-xsfYYg.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-b7ukMy0z.js → GraphInvokePage-Cf9c_LJ-.js} +2 -2
  54. package/dashboard/dist/assets/{GraphInvokePage-b7ukMy0z.js.map → GraphInvokePage-Cf9c_LJ-.js.map} +1 -1
  55. package/dashboard/dist/assets/HomePage-BdCtKXAV.js +2 -0
  56. package/dashboard/dist/assets/HomePage-BdCtKXAV.js.map +1 -0
  57. package/dashboard/dist/assets/ListToolbar-CDlol8XJ.js +2 -0
  58. package/dashboard/dist/assets/{ListToolbar-DDG6DXVF.js.map → ListToolbar-CDlol8XJ.js.map} +1 -1
  59. package/dashboard/dist/assets/{McpOverview-P2o3b3mi.js → McpOverview-ChZ36p9t.js} +2 -2
  60. package/dashboard/dist/assets/{McpOverview-P2o3b3mi.js.map → McpOverview-ChZ36p9t.js.map} +1 -1
  61. package/dashboard/dist/assets/{McpQueryDetailPage-C2CeBLb0.js → McpQueryDetailPage-X4j_jm3r.js} +2 -2
  62. package/dashboard/dist/assets/{McpQueryDetailPage-C2CeBLb0.js.map → McpQueryDetailPage-X4j_jm3r.js.map} +1 -1
  63. package/dashboard/dist/assets/{McpQueryPage-D_N-3lp0.js → McpQueryPage-C30HsQfZ.js} +2 -2
  64. package/dashboard/dist/assets/{McpQueryPage-D_N-3lp0.js.map → McpQueryPage-C30HsQfZ.js.map} +1 -1
  65. package/dashboard/dist/assets/{McpRunDetailPage-uDw3MWR_.js → McpRunDetailPage-18d7Iqfy.js} +2 -2
  66. package/dashboard/dist/assets/{McpRunDetailPage-uDw3MWR_.js.map → McpRunDetailPage-18d7Iqfy.js.map} +1 -1
  67. package/dashboard/dist/assets/{McpRunsPage-CDTUxghb.js → McpRunsPage-BHZHCwMM.js} +2 -2
  68. package/dashboard/dist/assets/{McpRunsPage-CDTUxghb.js.map → McpRunsPage-BHZHCwMM.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-GMojwL3q.js +2 -0
  74. package/dashboard/dist/assets/OperationsPage-GMojwL3q.js.map +1 -0
  75. package/dashboard/dist/assets/OperatorDashboard-BGJrxLMU.js +2 -0
  76. package/dashboard/dist/assets/{OperatorDashboard-CVRD8nQH.js.map → OperatorDashboard-BGJrxLMU.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-Dt6PAK04.js → ProcessDetailPage-Bcp3GKFo.js} +2 -2
  82. package/dashboard/dist/assets/{ProcessDetailPage-Dt6PAK04.js.map → ProcessDetailPage-Bcp3GKFo.js.map} +1 -1
  83. package/dashboard/dist/assets/{ProcessesListPage-CMkjASgs.js → ProcessesListPage-DPrvuLoU.js} +2 -2
  84. package/dashboard/dist/assets/{ProcessesListPage-CMkjASgs.js.map → ProcessesListPage-DPrvuLoU.js.map} +1 -1
  85. package/dashboard/dist/assets/RoleDetailPage-Ws3umQKQ.js +8 -0
  86. package/dashboard/dist/assets/RoleDetailPage-Ws3umQKQ.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-D3NNU92h.js +2 -0
  90. package/dashboard/dist/assets/{RolesPage-DtHIjDss.js.map → RolesPage-D3NNU92h.js.map} +1 -1
  91. package/dashboard/dist/assets/RunAsSelector-Z2Yunqdf.js +2 -0
  92. package/dashboard/dist/assets/{RunAsSelector-C198CpFs.js.map → RunAsSelector-Z2Yunqdf.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-CeLsnCIS.js +2 -0
  96. package/dashboard/dist/assets/{StickyPagination-eZ6WhMrL.js.map → StickyPagination-CeLsnCIS.js.map} +1 -1
  97. package/dashboard/dist/assets/{StreamMessageDetail-7xq_dddO.js → StreamMessageDetail-DWs7XkmH.js} +2 -2
  98. package/dashboard/dist/assets/{StreamMessageDetail-7xq_dddO.js.map → StreamMessageDetail-DWs7XkmH.js.map} +1 -1
  99. package/dashboard/dist/assets/{SwimlaneTimeline-Yg5QT0nI.js → SwimlaneTimeline-Dxl9a51q.js} +2 -2
  100. package/dashboard/dist/assets/{SwimlaneTimeline-Yg5QT0nI.js.map → SwimlaneTimeline-Dxl9a51q.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-Wc3AFnqs.js → TaskDetailPage-DVr31zDE.js} +2 -2
  104. package/dashboard/dist/assets/{TaskDetailPage-Wc3AFnqs.js.map → TaskDetailPage-DVr31zDE.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-V-1J_B_L.js → TasksListPage-DNGp5lfi.js} +2 -2
  108. package/dashboard/dist/assets/{TasksListPage-V-1J_B_L.js.map → TasksListPage-DNGp5lfi.js.map} +1 -1
  109. package/dashboard/dist/assets/{TimeAgo-XdHghs5F.js → TimeAgo-j2Q4w4lY.js} +2 -2
  110. package/dashboard/dist/assets/{TimeAgo-XdHghs5F.js.map → TimeAgo-j2Q4w4lY.js.map} +1 -1
  111. package/dashboard/dist/assets/{TimestampCell-BrzctPWT.js → TimestampCell-BLEfob2C.js} +2 -2
  112. package/dashboard/dist/assets/{TimestampCell-BrzctPWT.js.map → TimestampCell-BLEfob2C.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-CGfGUsaq.js → ToolTestPanel-ZJZT_mDP.js} +2 -2
  116. package/dashboard/dist/assets/{ToolTestPanel-CGfGUsaq.js.map → ToolTestPanel-ZJZT_mDP.js.map} +1 -1
  117. package/dashboard/dist/assets/{TopicDetailPage-CcAgD2k5.js → TopicDetailPage-ynUkO1A8.js} +3 -3
  118. package/dashboard/dist/assets/{TopicDetailPage-CcAgD2k5.js.map → TopicDetailPage-ynUkO1A8.js.map} +1 -1
  119. package/dashboard/dist/assets/{TopicsPage-BOQG7s4P.js → TopicsPage-Ctb_85Tw.js} +2 -2
  120. package/dashboard/dist/assets/{TopicsPage-BOQG7s4P.js.map → TopicsPage-Ctb_85Tw.js.map} +1 -1
  121. package/dashboard/dist/assets/{UserName-BzxR0Ihp.js → UserName-CkFpUqVK.js} +2 -2
  122. package/dashboard/dist/assets/{UserName-BzxR0Ihp.js.map → UserName-CkFpUqVK.js.map} +1 -1
  123. package/dashboard/dist/assets/{WorkflowExecutionPage-hRsDMByj.js → WorkflowExecutionPage-CSw2aQJC.js} +2 -2
  124. package/dashboard/dist/assets/{WorkflowExecutionPage-hRsDMByj.js.map → WorkflowExecutionPage-CSw2aQJC.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-ky0ulsUe.js → WorkflowsDashboard-DOyZbqzf.js} +2 -2
  128. package/dashboard/dist/assets/{WorkflowsDashboard-ky0ulsUe.js.map → WorkflowsDashboard-DOyZbqzf.js.map} +1 -1
  129. package/dashboard/dist/assets/{WorkflowsOverview-CzRJUV58.js → WorkflowsOverview-CRyHvUBx.js} +2 -2
  130. package/dashboard/dist/assets/{WorkflowsOverview-CzRJUV58.js.map → WorkflowsOverview-CRyHvUBx.js.map} +1 -1
  131. package/dashboard/dist/assets/{YamlWorkflowDetailPage-Beqzy9Ph.js → YamlWorkflowDetailPage-DRBmQXY6.js} +2 -2
  132. package/dashboard/dist/assets/{YamlWorkflowDetailPage-Beqzy9Ph.js.map → YamlWorkflowDetailPage-DRBmQXY6.js.map} +1 -1
  133. package/dashboard/dist/assets/{YamlWorkflowsPage-BzBUU2ug.js → YamlWorkflowsPage-HnI-9eBW.js} +2 -2
  134. package/dashboard/dist/assets/{YamlWorkflowsPage-BzBUU2ug.js.map → YamlWorkflowsPage-HnI-9eBW.js.map} +1 -1
  135. package/dashboard/dist/assets/{agents-Dlu4m6jy.js → agents-BA2CvQO9.js} +2 -2
  136. package/dashboard/dist/assets/{agents-Dlu4m6jy.js.map → agents-BA2CvQO9.js.map} +1 -1
  137. package/dashboard/dist/assets/{bots-C4G5U-6Q.js → bots-DHHnoo8x.js} +2 -2
  138. package/dashboard/dist/assets/{bots-C4G5U-6Q.js.map → bots-DHHnoo8x.js.map} +1 -1
  139. package/dashboard/dist/assets/{capabilities-BSFP_boq.js → capabilities-BYBKLRRI.js} +2 -2
  140. package/dashboard/dist/assets/{capabilities-BSFP_boq.js.map → capabilities-BYBKLRRI.js.map} +1 -1
  141. package/dashboard/dist/assets/{controlplane-qCqoP2ZV.js → controlplane-H0Ljhb-1.js} +2 -2
  142. package/dashboard/dist/assets/{controlplane-qCqoP2ZV.js.map → controlplane-H0Ljhb-1.js.map} +1 -1
  143. package/dashboard/dist/assets/escalation-columns-BV3Ztn7D.js +2 -0
  144. package/dashboard/dist/assets/escalation-columns-BV3Ztn7D.js.map +1 -0
  145. package/dashboard/dist/assets/index-B4FaugP1.js +2 -0
  146. package/dashboard/dist/assets/{index-BtEWQNcP.js.map → index-B4FaugP1.js.map} +1 -1
  147. package/dashboard/dist/assets/{index-D_FNaSoe.js → index-BI-9M7z3.js} +2 -2
  148. package/dashboard/dist/assets/{index-D_FNaSoe.js.map → index-BI-9M7z3.js.map} +1 -1
  149. package/dashboard/dist/assets/{index-CuC_mNNA.js → index-BXrm76Hm.js} +2 -2
  150. package/dashboard/dist/assets/{index-CuC_mNNA.js.map → index-BXrm76Hm.js.map} +1 -1
  151. package/dashboard/dist/assets/index-BeFjLCIQ.js +5 -0
  152. package/dashboard/dist/assets/index-BeFjLCIQ.js.map +1 -0
  153. package/dashboard/dist/assets/index-BrEcQQ1p.css +1 -0
  154. package/dashboard/dist/assets/{index-BObWYPxd.js → index-CP6D_Gtw.js} +2 -2
  155. package/dashboard/dist/assets/{index-BObWYPxd.js.map → index-CP6D_Gtw.js.map} +1 -1
  156. package/dashboard/dist/assets/index-CVMA5PVQ.js +2 -0
  157. package/dashboard/dist/assets/{index-C0KklOM4.js.map → index-CVMA5PVQ.js.map} +1 -1
  158. package/dashboard/dist/assets/{index-CGVqW6MI.js → index-CWkAN6wc.js} +2 -2
  159. package/dashboard/dist/assets/{index-CGVqW6MI.js.map → index-CWkAN6wc.js.map} +1 -1
  160. package/dashboard/dist/assets/index-D1Q8WfGN.js +2 -0
  161. package/dashboard/dist/assets/{index-QL26JxBF.js.map → index-D1Q8WfGN.js.map} +1 -1
  162. package/dashboard/dist/assets/{index-DUhettdV.js → index-DSjih1iX.js} +2 -2
  163. package/dashboard/dist/assets/{index-DUhettdV.js.map → index-DSjih1iX.js.map} +1 -1
  164. package/dashboard/dist/assets/{index-Dzhn5MDY.js → index-Dx6-4esw.js} +2 -2
  165. package/dashboard/dist/assets/{index-Dzhn5MDY.js.map → index-Dx6-4esw.js.map} +1 -1
  166. package/dashboard/dist/assets/index-O2wWcCLo.js +5 -0
  167. package/dashboard/dist/assets/{index-DWSNtg28.js.map → index-O2wWcCLo.js.map} +1 -1
  168. package/dashboard/dist/assets/{index-DCTWUswT.js → index-YWvrMcqh.js} +4 -4
  169. package/dashboard/dist/assets/index-YWvrMcqh.js.map +1 -0
  170. package/dashboard/dist/assets/index-iX8HLLB9.js +63 -0
  171. package/dashboard/dist/assets/index-iX8HLLB9.js.map +1 -0
  172. package/dashboard/dist/assets/{knowledge-CTdgLX5j.js → knowledge-BVbElRHf.js} +2 -2
  173. package/dashboard/dist/assets/{knowledge-CTdgLX5j.js.map → knowledge-BVbElRHf.js.map} +1 -1
  174. package/dashboard/dist/assets/{mcp-BEsMr678.js → mcp-CstDdowJ.js} +2 -2
  175. package/dashboard/dist/assets/{mcp-BEsMr678.js.map → mcp-CstDdowJ.js.map} +1 -1
  176. package/dashboard/dist/assets/{mcp-query-CmamHKIR.js → mcp-query-DHUs0N8H.js} +2 -2
  177. package/dashboard/dist/assets/{mcp-query-CmamHKIR.js.map → mcp-query-DHUs0N8H.js.map} +1 -1
  178. package/dashboard/dist/assets/{pipelines-taOXA2-y.js → pipelines-BDmAzE3U.js} +2 -2
  179. package/dashboard/dist/assets/{pipelines-taOXA2-y.js.map → pipelines-BDmAzE3U.js.map} +1 -1
  180. package/dashboard/dist/assets/{tasks-DEaDdyBh.js → tasks--qZocuG-.js} +2 -2
  181. package/dashboard/dist/assets/{tasks-DEaDdyBh.js.map → tasks--qZocuG-.js.map} +1 -1
  182. package/dashboard/dist/assets/{topics-2qRx68Ft.js → topics-P3jkjMsx.js} +2 -2
  183. package/dashboard/dist/assets/{topics-2qRx68Ft.js.map → topics-P3jkjMsx.js.map} +1 -1
  184. package/dashboard/dist/assets/{useEventHooks-oXjfKrbI.js → useEventHooks-CqH8Pasj.js} +2 -2
  185. package/dashboard/dist/assets/{useEventHooks-oXjfKrbI.js.map → useEventHooks-CqH8Pasj.js.map} +1 -1
  186. package/dashboard/dist/assets/{useNamespace-YdT1Zouk.js → useNamespace-aI3tPfo3.js} +2 -2
  187. package/dashboard/dist/assets/{useNamespace-YdT1Zouk.js.map → useNamespace-aI3tPfo3.js.map} +1 -1
  188. package/dashboard/dist/assets/{useYamlActivityEvents-Bt-RZ7Zl.js → useYamlActivityEvents-CPVyuW8i.js} +2 -2
  189. package/dashboard/dist/assets/{useYamlActivityEvents-Bt-RZ7Zl.js.map → useYamlActivityEvents-CPVyuW8i.js.map} +1 -1
  190. package/dashboard/dist/assets/{users-DbX5JZ_w.js → users-BrcmIH7O.js} +2 -2
  191. package/dashboard/dist/assets/{users-DbX5JZ_w.js.map → users-BrcmIH7O.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-acMcPTC5.js → workflows-DPhX5L5C.js} +2 -2
  195. package/dashboard/dist/assets/{workflows-acMcPTC5.js.map → workflows-DPhX5L5C.js.map} +1 -1
  196. package/dashboard/dist/assets/{yaml-workflows-D6kzrMYx.js → yaml-workflows-KZI2cufF.js} +2 -2
  197. package/dashboard/dist/assets/{yaml-workflows-D6kzrMYx.js.map → yaml-workflows-KZI2cufF.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 +195 -0
  210. package/docs/hitl/x-lt-widget.md +162 -0
  211. package/docs/hitl-guide.md +84 -1315
  212. package/docs/iam.md +1 -1
  213. package/package.json +2 -2
  214. package/dashboard/dist/assets/AvailableEscalationsPage-BVgMtftp.js +0 -2
  215. package/dashboard/dist/assets/AvailableEscalationsPage-BVgMtftp.js.map +0 -1
  216. package/dashboard/dist/assets/BotPicker-DJ0YYKZA.js +0 -2
  217. package/dashboard/dist/assets/BotPicker-DJ0YYKZA.js.map +0 -1
  218. package/dashboard/dist/assets/CapabilitiesPage-DFQryGBj.js +0 -2
  219. package/dashboard/dist/assets/CountdownTimer-BTJmh4Xw.js +0 -2
  220. package/dashboard/dist/assets/CountdownTimer-BTJmh4Xw.js.map +0 -1
  221. package/dashboard/dist/assets/CredentialsPage-BWVgxRpZ.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-K0U3yMdU.js +0 -2
  225. package/dashboard/dist/assets/HomePage-K0U3yMdU.js.map +0 -1
  226. package/dashboard/dist/assets/ListToolbar-DDG6DXVF.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-Daah4pNk.js +0 -2
  230. package/dashboard/dist/assets/OperationsPage-Daah4pNk.js.map +0 -1
  231. package/dashboard/dist/assets/OperatorDashboard-CVRD8nQH.js +0 -2
  232. package/dashboard/dist/assets/RoleDetailPage-6H1AY6sK.js +0 -8
  233. package/dashboard/dist/assets/RoleDetailPage-6H1AY6sK.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-DtHIjDss.js +0 -2
  237. package/dashboard/dist/assets/RunAsSelector-C198CpFs.js +0 -2
  238. package/dashboard/dist/assets/StickyPagination-eZ6WhMrL.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-Ck7ZOd7Z.js +0 -2
  242. package/dashboard/dist/assets/escalation-columns-Ck7ZOd7Z.js.map +0 -1
  243. package/dashboard/dist/assets/index-BtEWQNcP.js +0 -2
  244. package/dashboard/dist/assets/index-C0KklOM4.js +0 -2
  245. package/dashboard/dist/assets/index-CY3hpJR-.js +0 -5
  246. package/dashboard/dist/assets/index-CY3hpJR-.js.map +0 -1
  247. package/dashboard/dist/assets/index-DCTWUswT.js.map +0 -1
  248. package/dashboard/dist/assets/index-DWSNtg28.js +0 -5
  249. package/dashboard/dist/assets/index-DcQiD6OF.js +0 -63
  250. package/dashboard/dist/assets/index-DcQiD6OF.js.map +0 -1
  251. package/dashboard/dist/assets/index-QL26JxBF.js +0 -2
  252. package/dashboard/dist/assets/index-waKF7bKZ.css +0 -1
  253. package/dashboard/dist/assets/roles-BiUJAsVM.js +0 -2
  254. package/dashboard/dist/assets/roles-BiUJAsVM.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,1338 +40,87 @@ 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
50
+ ## What Long-tail Provides
106
51
 
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`** — lowest-priority fallback, used only when no role `form_schema` is available.
185
-
186
- ### Versioned Role Schemas
187
-
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
73
+ ## Section Map
207
74
 
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.
75
+ Ordered as a learning patheach file adds one capability to the same form:
209
76
 
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.
226
-
227
- ### Resolving a set atomically
228
-
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
96
  | `x-lt-widget` | field | Rich control: `file-upload`, `code-editor`, `signature`, `rich-text`, `markdown`, `checklist` |
249
- | `x-lt-source` | field | Data path for context-driven widgets: `"domain.path"` (e.g. `"envelope.checklist_items"`) |
250
- | `x-lt-language` | field | Syntax hint shown by the `code-editor` widget |
251
- | `accept` | field | File-type filter for the `file-upload` widget (e.g. `".pdf,.png"`) |
252
- | `x-lt-bind` | field | Path this field's value occupies in the resolver payload (e.g. `"customer.email"`) |
97
+ | `x-lt-source` | field | Data path for context-driven widgets: `"domain.path"` |
98
+ | `x-lt-require-all` | field | Checklist completion guard every item must be checked, except items declared `required: false` |
99
+ | `x-lt-language` | field | Syntax hint for the `code-editor` widget |
100
+ | `accept` | field | File-type filter for `file-upload` (e.g. `".pdf,.png"`) |
101
+ | `x-lt-bind` | field | Path in the resolver payload (e.g. `"customer.email"`) |
253
102
  | `x-lt-span` | field | Column span in a `two-column` layout (`2` = full width) |
254
- | `x-lt-showIf` | field | Show field only when a value exists at `domain.path`; prefix `!` to invert |
255
- | `x-lt-hide-if-empty` | field | `true` — suppress the field entirely when its value is null, `""`, `false`, or `0` |
256
- | `x-lt-section` | field | Label to group fields under (e.g. `"Facts"`, `"Station checks"`, `"Action"`) |
257
- | `x-lt-columns` | schema (list) | Column definitions for `facet-table` layout: `[{ label, value }]` |
103
+ | `x-lt-showIf` | field | Show field when a value is truthy at `domain.path`; prefix `!` to invert |
104
+ | `x-lt-hide-if-empty` | field | `true` — suppress the field when its value is null, `""`, `false`, or `0` |
105
+ | `x-lt-section` | field | Section group label |
106
+ | `x-lt-minimum` | field | Dynamic lower bound — resolves a `"domain.path"` from the escalation context |
107
+ | `x-lt-maximum` | field | Dynamic upper bound — resolves a `"domain.path"` from the escalation context |
108
+ | `x-lt-min-length` | field | Dynamic minimum string length — resolves a `"domain.path"` |
109
+ | `x-lt-max-length` | field | Dynamic maximum string length — resolves a `"domain.path"` |
110
+ | `x-lt-pattern-error` | field | Human-readable label for a `pattern` validation failure |
258
111
  | `x-lt-order` | schema | Field render sequence |
259
- | `x-lt-layout` | schema | `"two-column"` grid layout |
260
- | `x-lt-help` | schema | Markdown guidance for the side panel's Help view; `{{domain.path}}` tokens interpolate live record values |
112
+ | `x-lt-layout` | schema | `"two-column"` grid layout (form) or `"active-history"` / `"facet-table"` (list) |
113
+ | `x-lt-help` | schema | Markdown guidance for the side panel's Help view |
261
114
  | `x-lt-context` | schema | Plain-text fallback for the Help view when `x-lt-help` is absent |
262
115
  | `x-lt-viewport` | schema | Replace the generated form with a custom iframe UI |
116
+ | `x-lt-columns` | schema (list) | Column definitions for `facet-table` layout |
117
+ | `x-lt-active` | schema (list) | Active-item card definition |
118
+ | `x-lt-history` | schema (list) | History column definition |
263
119
  | `format` | field | Input specialization: `password`, `date`, `date-time`, `email`, `uri`, `textarea` |
264
- | `readOnly` | field | Static display (or a rendered content block with the `markdown` widget) |
265
- | `required` | schema | Fields that must be filled before submit |
266
- | `title` / `description` | both | Section header / helper text |
267
-
268
- Two working references ship as example workflows:
269
-
270
- - `examples/workflows/rich-form/` — the `intake-reviewer` role exercises the whole vocabulary: side-panel help, two-column layout, ordering, date and email formats, enum, file upload, spans, required fields, and `x-lt-bind`.
271
- - `examples/workflows/checklist-confirmation/` — the `checklist-operator` role demonstrates the `checklist` widget with `x-lt-source` driving a dynamic list of checkboxes from `envelope.checklist_items`.
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
- | `"checklist"` | Dynamic list of labeled checkboxes. Item definitions come from any context domain at runtime via `x-lt-source`. Stores `Record<string, boolean>` keyed by item id. |
320
-
321
- ```json
322
- {
323
- "properties": {
324
- "screenshot": {
325
- "type": "string",
326
- "x-lt-widget": "file-upload",
327
- "accept": "image/*",
328
- "description": "Upload a screenshot of the issue"
329
- },
330
- "fix_script": {
331
- "type": "string",
332
- "x-lt-widget": "code-editor",
333
- "x-lt-language": "sql",
334
- "description": "SQL migration to apply"
335
- },
336
- "signature": {
337
- "type": "string",
338
- "x-lt-widget": "signature",
339
- "description": "Sign to confirm"
340
- }
341
- }
342
- }
343
- ```
344
-
345
- #### Markdown content blocks
346
-
347
- `readOnly: true` + `x-lt-widget: "markdown"` turns a field into a rendered content
348
- block: the markdown in its `default` displays as HTML inside the form — headings,
349
- tables, checklists, callouts. The versioned schema carries the page source itself,
350
- so review instructions and SOPs version with the form they belong to, and the
351
- source rides along in the resolver payload like any read-only field.
352
-
353
- ```json
354
- {
355
- "properties": {
356
- "review_guide": {
357
- "type": "string",
358
- "readOnly": true,
359
- "x-lt-widget": "markdown",
360
- "x-lt-span": 2,
361
- "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."
362
- }
363
- }
364
- }
365
- ```
366
-
367
- Without `readOnly`, the field is a markdown *editor* — the resolver writes source in
368
- a Write/Preview toggle and the submitted value is the markdown text.
369
-
370
- #### Checklist widget (`x-lt-widget: "checklist"`)
371
-
372
- A dynamic list of labeled checkboxes driven by runtime data in the escalation context. The item definitions come from a domain path declared in `x-lt-source` — the form schema itself never changes as item count or labels vary across escalations.
373
-
374
- The field type must be `"object"`. The submitted value is `Record<string, boolean>` keyed by item id (e.g. `{ "item_0": true, "item_1": false }`).
375
-
376
- ```json
377
- {
378
- "properties": {
379
- "items": {
380
- "type": "object",
381
- "description": "Work through each step and check it off.",
382
- "x-lt-widget": "checklist",
383
- "x-lt-source": "envelope.checklist_items"
384
- }
385
- },
386
- "required": ["items"]
387
- }
388
- ```
389
-
390
- The `x-lt-source` value is `"domain.path"` — the same domain/path convention used by `x-lt-showIf` and `x-lt-help` tokens. The dashboard resolves the path at render time and expects an array of `{ id: string; label: string }` objects.
391
-
392
- **Domain choice:**
393
- - `envelope` — for item definitions that are render data only (no query cost). The workflow puts them in `conditionLT`'s `envelope` parameter.
394
- - `metadata` — only when items need to be GIN-indexed and searchable as facets. Adds index cost.
395
-
396
- **Workflow side:**
397
-
398
- ```typescript
399
- const checklistItems = [
400
- { id: 'item_0', label: 'Step 1: Verify patient ID' },
401
- { id: 'item_1', label: 'Step 2: Confirm dosage' },
402
- ];
403
-
404
- const decision = await conditionLT<{ items: Record<string, boolean> }>(signalId, {
405
- role: 'checklist-operator',
406
- envelope: {
407
- checklist_items: checklistItems,
408
- formDefaults: {
409
- items: Object.fromEntries(checklistItems.map((i) => [i.id, false])),
410
- },
411
- },
412
- });
413
-
414
- const allConfirmed = Object.values(decision.items).every(Boolean);
415
- ```
416
-
417
- Pre-populating `formDefaults.items` with `false` for each id opens the form with every checkbox unchecked rather than indeterminate.
418
-
419
- The `examples/workflows/checklist-confirmation/` workflow is the reference: it accepts a `count` input (1–20), generates that many items, suspends for a human to check them off, and returns a summary including `confirmed`, `unconfirmed`, and `allConfirmed`.
420
-
421
- ### Help Panel (`x-lt-help`)
422
-
423
- Schema-level `x-lt-help` carries the form's guidance — checklists, tier tables,
424
- callouts, links — as markdown. The dashboard renders it in the side panel beside the
425
- form, so the form itself stays a clean title and fields while the SOP sits one glance
426
- to the right. The help versions with the form: it lives in the same `form_schema`
427
- snapshot in `lt_role_schemas`.
428
-
429
- ```json
430
- {
431
- "title": "Customer Intake",
432
- "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)",
433
- "properties": { ... }
434
- }
435
- ```
436
-
437
- `{{domain.path}}` tokens interpolate live values from the escalation surface using the
438
- `x-lt-bind` path syntax (dot keys, optional `[n]` indices). Five domains are available:
439
-
440
- | Domain | Resolves against |
441
- |--------|------------------|
442
- | `escalation` | The escalation row (`{{escalation.role}}`, `{{escalation.status}}`) |
443
- | `metadata` | The row's metadata dict (`{{metadata.schema_version}}`) |
444
- | `envelope` | The workflow-sent input envelope (`{{envelope.formDefaults.customer.name}}`) |
445
- | `payload` | The escalation context payload (`{{payload.category}}`) |
446
- | `resolver` | The submitted resolver payload (`{{resolver.notes}}`) |
447
-
448
- A missing value renders as an em dash. Links whose href starts with `/` navigate
449
- inside the dashboard.
450
-
451
- The Help view falls back in order: `x-lt-help` → `x-lt-context` (plain text) → a
452
- state-aware hint ("Claim this escalation to enable the form", "Fill out the form and
453
- submit to resolve it", and so on), so the panel always tells the resolver what the
454
- page expects of them. The panel's other views surface the record itself: **Metadata**
455
- (the row's metadata values), **Context** (input envelope, escalation context, resolver
456
- payload), and **Record** (the raw escalation JSON, builders only).
457
-
458
- ### Payload Binding (`x-lt-bind`)
459
-
460
- The form is flat; the payload the workflow consumes rarely is. A field may declare
461
- `x-lt-bind` — the path its value occupies in the resolver payload (dot keys, optional
462
- `[n]` indices). The dashboard maps the flat form through the binds on submit, and
463
- reverse-maps workflow-seeded `envelope.formDefaults` through them to prefill. A field
464
- with no bind lands at its own name at the payload root (1:1).
465
-
466
- ```json
467
- {
468
- "properties": {
469
- "customer_name": { "type": "string", "x-lt-bind": "customer.name" },
470
- "contact_email": { "type": "string", "format": "email", "x-lt-bind": "customer.email" },
471
- "tier": { "type": "string", "enum": ["starter", "professional"], "x-lt-bind": "contract.tier" },
472
- "notes": { "type": "string", "format": "textarea" }
473
- }
474
- }
475
- ```
476
-
477
- Submitting `{ customer_name, contact_email, tier, notes }` stores:
478
-
479
- ```json
480
- {
481
- "customer": { "name": "…", "email": "…" },
482
- "contract": { "tier": "…" },
483
- "notes": "…"
484
- }
485
- ```
486
-
487
- Only the FORM is versioned on the role — the payload shape is the workflow's own
488
- contract, produced by the binds. Evolve the form and its binds together, and the
489
- workflow's resolver type in the same commit.
490
-
491
- ### Layout Options (`x-lt-layout`)
492
-
493
- Control how fields are arranged:
494
-
495
- | Layout | Behavior |
496
- |--------|----------|
497
- | `"two-column"` | Fields in a 2-column grid. Use `x-lt-span: 2` on a field for full-width. |
498
-
499
- ```json
500
- {
501
- "x-lt-layout": "two-column",
502
- "properties": {
503
- "first_name": { "type": "string" },
504
- "last_name": { "type": "string" },
505
- "notes": { "type": "string", "format": "textarea", "x-lt-span": 2 }
506
- }
507
- }
508
- ```
509
-
510
- ### Field Ordering (`x-lt-order`)
511
-
512
- By default, fields render in JSON key order. Use `x-lt-order` to control sequence:
513
-
514
- ```json
515
- {
516
- "x-lt-order": ["priority", "decision", "notes"],
517
- "properties": {
518
- "notes": { "type": "string" },
519
- "decision": { "type": "string", "enum": ["approve", "reject", "defer"] },
520
- "priority": { "type": "number" }
521
- }
522
- }
523
- ```
524
-
525
- ### Conditional Visibility (`x-lt-showIf`)
526
-
527
- 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:
528
-
529
- ```json
530
- "x-lt-showIf": "domain.path"
531
- ```
532
-
533
- 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.
534
-
535
- Domains follow the same `domain.path` convention as `x-lt-help` tokens:
536
-
537
- | Domain | Resolves against |
538
- |--------|-----------------|
539
- | `metadata` | The row's metadata dict |
540
- | `payload` | The escalation context payload (`escalation_payload`) |
541
- | `envelope` | The workflow-sent input envelope |
542
- | `escalation` | Top-level escalation row fields (`role`, `status`, `priority`, …) |
543
- | `resolver` | The submitted resolver payload |
544
-
545
- **Example — item type branching:**
546
-
547
- A role where the queue receives both regular work items and crew-pill shutdown signals. The payload carries `item_type` to distinguish them.
548
-
549
- ```json
550
- {
551
- "title": "Worker Station",
552
- "properties": {
553
- "action_taken": {
554
- "type": "string",
555
- "enum": ["completed", "deferred", "escalated"],
556
- "description": "Outcome for this work item",
557
- "x-lt-showIf": "!payload.crew_pill"
558
- },
559
- "notes": {
560
- "type": "string",
561
- "format": "textarea",
562
- "x-lt-showIf": "!payload.crew_pill"
563
- },
564
- "shutdown_ack": {
565
- "type": "boolean",
566
- "title": "Acknowledge shutdown",
567
- "description": "Confirm you are stopping work and clearing the station",
568
- "x-lt-showIf": "payload.crew_pill"
569
- }
570
- }
571
- }
572
- ```
573
-
574
- 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.
575
-
576
- `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.
577
-
578
- **Example — approval/rejection conditional fields:**
579
-
580
- 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`:
581
-
582
- ```json
583
- {
584
- "title": "Rejection Review",
585
- "x-lt-layout": "two-column",
586
- "x-lt-order": ["order_id", "rejection_type", "approved", "rejection_reason", "rejection_notes"],
587
- "required": ["approved"],
588
- "properties": {
589
- "order_id": { "type": "string", "readOnly": true, "x-lt-section": "The Report" },
590
- "rejection_type": { "type": "string", "readOnly": true, "x-lt-section": "The Report" },
591
- "approved": { "type": "boolean", "x-lt-section": "The Verdict" },
592
- "rejection_reason": {
593
- "type": "string",
594
- "enum": ["Quality", "Quantity", "Routing", "Other"],
595
- "x-lt-showIf": "!resolver.approved",
596
- "x-lt-section": "The Verdict"
597
- },
598
- "rejection_notes": {
599
- "type": "string",
600
- "format": "textarea",
601
- "x-lt-span": 2,
602
- "x-lt-showIf": "!resolver.approved",
603
- "x-lt-section": "The Verdict"
604
- }
605
- }
606
- }
607
- ```
608
-
609
- In the workflow, branch on `approved` from the resolver payload:
610
-
611
- ```typescript
612
- const decision = await conditionLT<{
613
- approved: boolean;
614
- rejection_reason?: string;
615
- rejection_notes?: string;
616
- }>(signalId, { role: 'quality-reviewer', /* ... */ });
617
-
618
- if (!decision.approved) {
619
- await sendForRework({ reason: decision.rejection_reason, notes: decision.rejection_notes });
620
- } else {
621
- await advanceOrder();
622
- }
623
- ```
624
-
625
- ### Suppressing Empty Fields (`x-lt-hide-if-empty`)
626
-
627
- 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:
628
-
629
- ```json
630
- {
631
- "properties": {
632
- "heel_raise": {
633
- "type": "string",
634
- "readOnly": true,
635
- "x-lt-hide-if-empty": true,
636
- "description": "Heel raise specification — hidden when not present on this order"
637
- }
638
- }
639
- }
640
- ```
641
-
642
- The field remains in form state and is included in the submitted payload if it has a value. Only the visual row is suppressed.
643
-
644
- ### Section Headers (`x-lt-section`)
645
-
646
- 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:
647
-
648
- ```json
649
- {
650
- "x-lt-layout": "two-column",
651
- "x-lt-order": ["patient_id", "heel_cup", "pdac", "approved", "notes"],
652
- "properties": {
653
- "patient_id": { "type": "string", "readOnly": true, "x-lt-section": "Facts", "x-lt-hide-if-empty": true },
654
- "heel_cup": { "type": "string", "readOnly": true, "x-lt-section": "Facts", "x-lt-hide-if-empty": true },
655
- "pdac": { "type": "boolean", "readOnly": true, "x-lt-section": "Facts", "x-lt-hide-if-empty": true },
656
- "approved": { "type": "string", "enum": ["yes", "no"], "x-lt-section": "Action" },
657
- "notes": { "type": "string", "format": "textarea", "x-lt-span": 2, "x-lt-section": "Action" }
658
- }
659
- }
660
- ```
661
-
662
- 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.
663
-
664
- ### Validation (`required`)
665
-
666
- Fields listed in `required` show a red asterisk and block submission when empty:
667
-
668
- ```json
669
- {
670
- "required": ["decision"],
671
- "properties": {
672
- "decision": { "type": "string", "enum": ["approve", "reject"] },
673
- "notes": { "type": "string", "description": "Optional comments" }
674
- }
675
- }
676
- ```
677
-
678
- ### Read-Only Fields (`readOnly`)
679
-
680
- Fields with `readOnly: true` display as static text. Useful for showing context alongside editable fields:
681
-
682
- ```json
683
- {
684
- "properties": {
685
- "request_amount": { "type": "number", "readOnly": true },
686
- "approved_amount": { "type": "number", "description": "Enter the approved amount" }
687
- }
688
- }
689
- ```
690
-
691
- ### Schema Title and Description
692
-
693
- The `title` and `description` at the schema root are used in the UI:
694
- - **`title`**: Shown as the form's section header
695
- - **`description`**: Shown as helper text beneath the title — keep it to a short phrase; longer guidance belongs in `x-lt-help`
696
- - **`x-lt-help`**: Rendered as markdown in the side panel beside the form
697
-
698
- ```json
699
- {
700
- "title": "Expense Approval",
701
- "description": "Review the expense report below. Verify receipts match the claimed amounts. Approve or reject with notes.",
702
- "properties": { ... }
703
- }
704
- ```
705
-
706
- ---
707
-
708
- ## Iframe Viewport Protocol
709
-
710
- For fully custom UIs (PDF viewers, complex multi-step forms, specialized domain UIs), use an iframe viewport.
711
-
712
- ### Schema Declaration
713
-
714
- ```json
715
- {
716
- "x-lt-viewport": {
717
- "type": "iframe",
718
- "src": "https://your-app.example.com/hitl-form"
719
- },
720
- "properties": { ... }
721
- }
722
- ```
723
-
724
- When `x-lt-viewport` is present, the dashboard renders an iframe instead of the standard form.
725
-
726
- ### Message Protocol
727
-
728
- Communication happens via `window.postMessage`.
729
-
730
- #### Parent to Iframe
731
-
732
- ```typescript
733
- // Sent when the iframe signals ready or on load
734
- {
735
- type: 'lt:init',
736
- escalation: {
737
- id: string,
738
- type: string,
739
- subtype: string,
740
- description: string | null,
741
- status: string,
742
- priority: number,
743
- role: string,
744
- workflow_type: string | null,
745
- },
746
- schema: Record<string, unknown>, // The full form schema
747
- }
748
-
749
- // Optional: parent requests the iframe to submit
750
- {
751
- type: 'lt:requestSubmit'
752
- }
753
- ```
754
-
755
- #### Iframe to Parent
756
-
757
- ```typescript
758
- // Signal that the iframe is ready to receive init data
759
- { type: 'lt:ready' }
760
-
761
- // Submit the human's response — triggers escalation resolution
762
- { type: 'lt:submit', payload: { approved: true, notes: '...' } }
763
-
764
- // Escalate to a different role
765
- { type: 'lt:escalate', target: 'senior-reviewer' }
766
-
767
- // Auto-resize the iframe
768
- { type: 'lt:resize', height: 600 }
769
- ```
770
-
771
- ### Minimal Example
772
-
773
- ```html
774
- <!DOCTYPE html>
775
- <html>
776
- <head><title>Custom HITL Form</title></head>
777
- <body>
778
- <div id="form"></div>
779
- <button id="submit">Approve</button>
780
-
781
- <script>
782
- // Signal ready
783
- window.parent.postMessage({ type: 'lt:ready' }, '*');
784
-
785
- // Receive init data
786
- window.addEventListener('message', (event) => {
787
- if (event.data.type === 'lt:init') {
788
- const { escalation, schema } = event.data;
789
- document.getElementById('form').textContent =
790
- `Reviewing: ${escalation.description}`;
791
- }
792
- });
793
-
794
- // Submit response
795
- document.getElementById('submit').addEventListener('click', () => {
796
- window.parent.postMessage({
797
- type: 'lt:submit',
798
- payload: { approved: true, reviewed_at: new Date().toISOString() },
799
- }, '*');
800
- });
801
- </script>
802
- </body>
803
- </html>
804
- ```
805
-
806
- ### Security
807
-
808
- - The iframe runs with `sandbox="allow-scripts allow-same-origin allow-forms"`
809
- - The parent validates message origins — only messages from the iframe's origin are accepted
810
- - The `envelope` field (which may contain secrets) is NOT sent to the iframe
811
- - Only safe escalation metadata (id, type, description, status, priority, role) is exposed
812
-
813
- ---
814
-
815
- ## The Escalation Detail Page
816
-
817
- The escalation detail page has one view, built for the person resolving the item: the
818
- escalation's description is the page title, the form starts directly beneath it, and
819
- the action bar closes the page. The lifecycle sparkline (waiting / claimed / resolved
820
- ratios) sits as a short persistent row above the side panel. Everything else lives in
821
- the side panel, ordered by specificity:
822
-
823
- | View | Shows | Available to |
824
- |------|-------|--------------|
825
- | **Help** | The form's `x-lt-help` markdown, or a state-aware hint | Everyone |
826
- | **Details** | Status, role, priority, claim provenance, timestamps; identifier links below a divider | Everyone (identifiers: builders) |
827
- | **AI Analysis** | What triage diagnosed and corrected | When AI is enabled and triage data is present |
828
- | **Metadata** | The row's metadata values | Everyone |
829
- | **Context** | Input envelope, escalation context, resolver payload | Everyone |
830
- | **Record** | The raw escalation JSON | Builders (admins, superadmins, engineers) |
831
-
832
- The page is two fixed-height columns beneath the global toolbar — the form column and
833
- the panel each scroll independently, so the panel stays pinned like the left nav while
834
- long forms or long panel content scroll. The form column narrows as the panel expands
835
- (the panel is capped at half the page). When the form carries `x-lt-help`, the panel
836
- opens expanded on the Help view; otherwise it stays hidden until the page-header panel
837
- button summons it.
838
-
839
- ### Designing the Form
840
-
841
- To create a polished resolve experience:
842
-
843
- 1. Set `title` on your schema — it replaces the section header
844
- 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
845
- 3. Use `readOnly` fields for context the human needs to see but shouldn't edit
846
- 4. Use `x-lt-order` to put the most important fields first
847
- 5. Use `required` to guide users on what must be filled
848
- 6. Use descriptive `description` on individual fields for inline help text
849
-
850
- ---
851
-
852
- ## Escalations List Schema
853
-
854
- The form schema formats one escalation on the detail page. A role can also own a
855
- `list_schema` that formats its whole **list** page — the list-page analog of the
856
- resolve form. It is opt-in and applies only when the list is scoped to exactly one
857
- role (`/escalations/available?role=<role>`). Absent, the list renders the standard
858
- engineer table; present, a rich role-authored view renders with a "Table view" toggle
859
- one click away. It is versioned **independently** of the form schema (its own timeline;
860
- a list edit never bumps the form version) and edited on its own page,
861
- `/admin/roles/:role/list-schema`. The list always renders the latest version.
862
-
863
- This is what turns a queue like a `policy-document` role — where a looped workflow
864
- keeps exactly one escalation live and each resolved one is a revision — into a document
865
- with a history, instead of a one-row table.
866
-
867
- ### Vocabulary
868
-
869
- Every string is a markdown/text template run through the same `{{domain.path}}` token
870
- binding as `x-lt-help` (domains `escalation | metadata | envelope | payload | resolver`,
871
- evaluated against each row); `body` strings render through the markdown renderer.
872
-
873
- | Key | Level | Purpose |
874
- |-----|-------|---------|
875
- | `x-lt-layout` | schema | `"active-history"` (two columns), `"active"` (card only), `"facet-table"` (multi-row queue), or `"table"` (fallback) |
876
- | `x-lt-help` | schema | Optional markdown header, interpolated with the active row |
877
- | `x-lt-active` | schema | The live item card: `{ title, subtitle?, body?, fields?: [{label, value}] }` |
878
- | `x-lt-history` | schema | History column: `{ row: { title, subtitle?, meta? }, limit?, status? }` |
879
- | `x-lt-columns` | schema | Column definitions for `facet-table` layout: `[{ label: string, value: string }]` — `value` is a `{{domain.path}}` token |
880
-
881
- The **active** item is the first non-terminal escalation. The **history** column is not
882
- auto-loaded — a "Load full history" link fetches resolved items on demand (`status`
883
- defaults to `resolved`, `limit` to 25). Unknown/absent `x-lt-layout` is a safe no-op
884
- that falls back to the table.
885
-
886
- ### Example — a policy-document role
887
-
888
- ```json
889
- {
890
- "x-lt-layout": "active-history",
891
- "x-lt-help": "# {{metadata.title}}\nThe authoritative policy. One revision is live at a time.",
892
- "x-lt-active": {
893
- "title": "{{metadata.title}}",
894
- "subtitle": "Revision {{metadata.revision}} · effective {{metadata.effective_date}}",
895
- "body": "{{metadata.document_markdown}}",
896
- "fields": [
897
- { "label": "Owner", "value": "{{metadata.owner}}" },
898
- { "label": "Claimed by", "value": "{{escalation.assigned_to}}" }
899
- ]
900
- },
901
- "x-lt-history": {
902
- "row": { "title": "{{metadata.title}} — revision {{metadata.revision}}" },
903
- "limit": 25
904
- }
905
- }
906
- ```
907
-
908
- The working reference is `examples/workflows/policy-document/` (role seeded by
909
- `examples/seed-policy-document.ts`): a looped workflow opens one policy-review
910
- escalation, parks on it, and folds each resolution into the next revision — so the
911
- policy facts ride the row's metadata and the list view reads them with `{{metadata.*}}`.
912
-
913
- ### `facet-table` layout — queue as a column table
914
-
915
- Use `"x-lt-layout": "facet-table"` when the queue contains many concurrent rows and the
916
- role's context is best expressed as a scannable table — a print farm, order queue, or
917
- batch-processing pond. Every pending escalation is a row; columns are defined by
918
- `x-lt-columns`.
919
-
920
- ```json
921
- {
922
- "x-lt-layout": "facet-table",
923
- "x-lt-columns": [
924
- { "label": "Patient", "value": "{{metadata.patientId}}" },
925
- { "label": "Heel cup", "value": "{{metadata.heelCup}}" },
926
- { "label": "PDAC", "value": "{{metadata.pdac}}" },
927
- { "label": "Station", "value": "{{metadata.station}}" },
928
- { "label": "Priority", "value": "{{escalation.priority}}" },
929
- { "label": "Created", "value": "{{escalation.created_at}}" }
930
- ]
931
- }
932
- ```
933
-
934
- A status dot precedes the first column automatically. ISO datetime values
935
- (`{{escalation.created_at}}`, `{{escalation.resolved_at}}`, etc.) render as a
936
- readable relative date with a full-timestamp tooltip. Missing/unresolvable token values
937
- render as an em dash. Clicking any row navigates to the detail page. `x-lt-help` and
938
- `x-lt-active` are ignored in this layout.
939
-
940
- ---
941
-
942
- ## Role-Based Routing
943
-
944
- Escalations are routed by role. Users only see escalations for roles they hold.
945
-
946
- ```typescript
947
- // Workflow escalates to a specific role
948
- await ltCreateEscalation({
949
- role: 'finance-reviewer', // Only users with this role see it
950
- // ...
951
- });
952
- ```
953
-
954
- ### Work-Surface Scope
955
-
956
- 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.
957
-
958
- 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.
959
-
960
- ### One-Time and Pre-Assigned Users
961
-
962
- 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.
963
-
964
- ```typescript
965
- // Pre-assign the escalation to a specific person and route a one-time form to them
966
- await ltCreateEscalation({
967
- role: 'customer-triage',
968
- assigned_to: userId, // pre-claim — durable, keyed off the user, not the soft-lock TTL
969
- description: 'Confirm your shipping address',
970
- metadata: {
971
- form_schema: {
972
- title: 'Confirm Address',
973
- properties: { address: { type: 'string' }, confirmed: { type: 'boolean' } },
974
- required: ['confirmed'],
975
- },
976
- },
977
- });
978
- // The person is provisioned as a member of `customer-triage` with read_scope=self, write_scope=self.
979
- ```
980
-
981
- ### Escalation Chains
982
-
983
- Users can escalate to other roles via the "Escalate" tab:
984
-
985
- ```
986
- Analyst → Senior Analyst → Manager → VP
987
- ```
988
-
989
- Configure escalation targets in the role configuration (Admin > Roles). Each role defines which other roles it can escalate to.
990
-
991
- ### Multi-Tier Example
992
-
993
- ```typescript
994
- // Level 1: Auto-review
995
- const result = await autoReview(document);
996
-
997
- if (result.confidence < 0.8) {
998
- // Level 2: Human analyst
999
- await ltCreateEscalation({
1000
- role: 'analyst',
1001
- description: `Low confidence review (${result.confidence})`,
1002
- metadata: {
1003
- form_schema: {
1004
- title: 'Document Review',
1005
- properties: {
1006
- approved: { type: 'boolean' },
1007
- corrections: { type: 'string', format: 'textarea' },
1008
- },
1009
- required: ['approved'],
1010
- },
1011
- },
1012
- });
1013
- // User can further escalate to 'senior-analyst' or 'manager' from the dashboard
1014
- }
1015
- ```
1016
-
1017
- ---
1018
-
1019
- ## Worked Examples
1020
-
1021
- Each example is a complete `conditionLT` call. The form schema lives in `metadata.form_schema` inside the config; the signal key is the first argument and is never duplicated in metadata.
1022
-
1023
- ### Simple Approval
1024
-
1025
- A workflow pauses for a yes/no decision. The resolver's payload shape is declared as the generic type parameter so the workflow is fully typed after resume.
1026
-
1027
- ```typescript
1028
- import { conditionLT } from '@hotmeshio/long-tail';
1029
-
1030
- export async function approveSpendWorkflow(envelope: LTEnvelope) {
1031
- const ctx = Durable.workflow.workflowInfo();
1032
-
1033
- const decision = await conditionLT<{ approved: boolean; notes?: string }>(
1034
- `spend-approval-${ctx.workflowId}`,
1035
- {
1036
- role: 'finance-reviewer',
1037
- type: 'approval',
1038
- description: `Approve spend of $${envelope.data.amount} for ${envelope.data.vendor}`,
1039
- priority: 2,
1040
- metadata: {
1041
- form_schema: {
1042
- title: 'Spend Approval',
1043
- description: 'Review and approve or reject this spend request.',
1044
- required: ['approved'],
1045
- properties: {
1046
- approved: { type: 'boolean', description: 'Check to approve' },
1047
- notes: { type: 'string', format: 'textarea', description: 'Optional comments' },
1048
- },
1049
- },
1050
- },
1051
- timeout: '48h',
1052
- },
1053
- );
1054
-
1055
- if (decision === null) return { type: 'return' as const, data: { cancelled: true } };
1056
- if (decision === false) return { type: 'return' as const, data: { timedOut: true } };
1057
-
1058
- if (decision.approved) {
1059
- await releasePayment(envelope.data);
1060
- } else {
1061
- await notifyRejection(envelope.data, decision.notes);
1062
- }
1063
- }
1064
- ```
1065
-
1066
- ### Multi-Field Intake with Layout
1067
-
1068
- A structured intake form — two-column layout, ordered fields, required validation, and `x-lt-help` guidance in the side panel.
1069
-
1070
- ```typescript
1071
- import { conditionLT } from '@hotmeshio/long-tail';
1072
-
1073
- export async function customerIntakeWorkflow(envelope: LTEnvelope) {
1074
- const ctx = Durable.workflow.workflowInfo();
1075
-
1076
- const intake = await conditionLT<{
1077
- first_name: string;
1078
- last_name: string;
1079
- email: string;
1080
- tier: 'free' | 'pro' | 'enterprise';
1081
- notes?: string;
1082
- }>(
1083
- `intake-${ctx.workflowId}`,
1084
- {
1085
- role: 'intake-reviewer',
1086
- type: 'intake',
1087
- description: 'New customer intake',
1088
- priority: 2,
1089
- envelope: { data: envelope.data },
1090
- metadata: {
1091
- form_schema: {
1092
- title: 'Customer Intake',
1093
- description: 'Complete all required fields then submit.',
1094
- 'x-lt-layout': 'two-column',
1095
- 'x-lt-order': ['first_name', 'last_name', 'email', 'phone', 'tier', 'notes'],
1096
- 'x-lt-help': '### Intake checklist\n\n1. Confirm the **legal name** matches the ID on file.\n2. Verify the email is reachable — send a test message.\n3. Select tier based on the sales order.\n\n> Escalate non-standard contract language to legal.',
1097
- required: ['first_name', 'last_name', 'email', 'tier'],
1098
- properties: {
1099
- first_name: { type: 'string' },
1100
- last_name: { type: 'string' },
1101
- email: { type: 'string', format: 'email' },
1102
- phone: { type: 'string' },
1103
- tier: {
1104
- type: 'string',
1105
- enum: ['free', 'pro', 'enterprise'],
1106
- description: 'Select the customer tier',
1107
- },
1108
- notes: {
1109
- type: 'string',
1110
- format: 'textarea',
1111
- 'x-lt-span': 2,
1112
- description: 'Additional notes',
1113
- },
1114
- },
1115
- },
1116
- },
1117
- },
1118
- );
1119
-
1120
- if (!intake) return { type: 'return' as const, data: { cancelled: true } };
1121
-
1122
- await provisionAccount({ ...intake });
1123
- }
1124
- ```
1125
-
1126
- ### Checklist Confirmation
1127
-
1128
- A dynamic checklist where item labels come from the workflow's envelope, not the static schema. Item count and wording can vary per escalation without ever touching the form schema.
1129
-
1130
- ```typescript
1131
- import { conditionLT } from '@hotmeshio/long-tail';
1132
-
1133
- export async function stationCheckWorkflow(envelope: LTEnvelope) {
1134
- const ctx = Durable.workflow.workflowInfo();
1135
-
1136
- const steps = [
1137
- { id: 'step_0', label: 'Verify patient ID matches the order' },
1138
- { id: 'step_1', label: 'Confirm dosage and route of administration' },
1139
- { id: 'step_2', label: 'Sign the dispensing log' },
1140
- ];
1141
-
1142
- const result = await conditionLT<{ checks: Record<string, boolean> }>(
1143
- `station-check-${ctx.workflowId}`,
1144
- {
1145
- role: 'station-operator',
1146
- type: 'station-check',
1147
- description: 'Complete all station checks before releasing',
1148
- priority: 1,
1149
- envelope: {
1150
- checklist_items: steps,
1151
- formDefaults: {
1152
- // Pre-populate all checkboxes as unchecked
1153
- checks: Object.fromEntries(steps.map((s) => [s.id, false])),
1154
- },
1155
- },
1156
- metadata: {
1157
- form_schema: {
1158
- title: 'Station Check',
1159
- description: 'Work through each step and confirm completion.',
1160
- required: ['checks'],
1161
- properties: {
1162
- checks: {
1163
- type: 'object',
1164
- 'x-lt-widget': 'checklist',
1165
- 'x-lt-source': 'envelope.checklist_items',
1166
- description: 'Check each step when complete',
1167
- },
1168
- },
1169
- },
1170
- },
1171
- },
1172
- );
1173
-
1174
- if (!result) return { type: 'return' as const, data: { cancelled: true } };
1175
-
1176
- const allClear = Object.values(result.checks).every(Boolean);
1177
- const failed = steps.filter((s) => !result.checks[s.id]).map((s) => s.label);
1178
-
1179
- if (!allClear) {
1180
- await flagIncomplete({ failed });
1181
- }
1182
- }
1183
- ```
1184
-
1185
- ### Credential Provisioning
1186
-
1187
- Password fields are automatically redacted and replaced with ephemeral tokens (15-min TTL) before being sent back to the workflow.
1188
-
1189
- ```typescript
1190
- import { conditionLT } from '@hotmeshio/long-tail';
1191
-
1192
- export async function credentialProvisionWorkflow(envelope: LTEnvelope) {
1193
- const ctx = Durable.workflow.workflowInfo();
1194
-
1195
- const creds = await conditionLT<{
1196
- api_key: string;
1197
- api_secret: string; // arrives as eph:v1:* token, never plain text
1198
- environment: 'sandbox' | 'production';
1199
- }>(
1200
- `creds-${ctx.workflowId}`,
1201
- {
1202
- role: 'integration-admin',
1203
- type: 'credential-provision',
1204
- description: `Provide API credentials for ${envelope.data.integration}`,
1205
- priority: 1,
1206
- metadata: {
1207
- form_schema: {
1208
- title: 'Provide Credentials',
1209
- description: 'Enter the API credentials. Secrets are encrypted in transit and stored as ephemeral tokens.',
1210
- required: ['api_key', 'api_secret', 'environment'],
1211
- properties: {
1212
- api_key: { type: 'string', description: 'API Key' },
1213
- api_secret: {
1214
- type: 'string',
1215
- format: 'password',
1216
- description: 'API Secret — redacted before storage, returned as an ephemeral token',
1217
- },
1218
- environment: {
1219
- type: 'string',
1220
- enum: ['sandbox', 'production'],
1221
- description: 'Target environment',
1222
- },
1223
- },
1224
- },
1225
- },
1226
- timeout: '24h',
1227
- },
1228
- );
1229
-
1230
- if (!creds) return { type: 'return' as const, data: { cancelled: true } };
1231
-
1232
- // creds.api_secret is an eph:v1:* token — pass it to the integration layer for redemption
1233
- await configureIntegration(envelope.data.integration, creds);
1234
- }
1235
- ```
1236
-
1237
- ---
1238
-
1239
- ## Resolving from System Code
1240
-
1241
- 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.
1242
-
1243
- ### By escalation ID
1244
-
1245
- Use when you already have the escalation UUID (e.g. stored in your own DB alongside the order):
1246
-
1247
- ```typescript
1248
- const result = await lt.escalations.resolve({
1249
- id: escalationId,
1250
- resolverPayload: { approved: true, targetStatus: 'ready' },
1251
- });
1252
- ```
1253
-
1254
- This routes through the full resolution path and works for all escalation types — atomic `conditionLT` (signal_key), two-step `conditionLT` (signal_id), and re-run-style escalations.
1255
-
1256
- ### By metadata key-value pair
1257
-
1258
- 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:
1259
-
1260
- ```typescript
1261
- const result = await lt.escalations.resolveByMetadata({
1262
- key: 'orderId',
1263
- value: orderId,
1264
- resolverPayload: { approved: true, targetStatus: 'ready' },
1265
- });
1266
-
1267
- if (result.status === 404) {
1268
- // No pending escalation for this orderId
1269
- }
1270
- ```
1271
-
1272
- 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.
1273
-
1274
- ### By signal key
1275
-
1276
- 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:
1277
-
1278
- ```typescript
1279
- await lt.escalations.resolveBySignalKey({
1280
- signalKey: `station-done-${workflowId}`,
1281
- resolverPayload: { approved: true },
1282
- });
1283
- ```
1284
-
1285
- ---
1286
-
1287
- ## Cancelling Escalations
1288
-
1289
- Escalations can be cancelled at any point before they are resolved. Cancellation is terminal — a cancelled escalation cannot be re-opened.
1290
-
1291
- ### When cancellation happens
1292
-
1293
- - **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`.
1294
- - **Explicit cancel** — cancel a single escalation via the API or from the dashboard. Any workflow waiting on that escalation via `conditionLT` receives `null`.
1295
-
1296
- ### API
1297
-
1298
- ```
1299
- POST /api/escalations/:id/cancel # single escalation
1300
- POST /api/escalations/bulk-cancel # { "ids": [...] }
1301
- ```
1302
-
1303
- Returns 409 if the escalation is already resolved or cancelled.
1304
-
1305
- ### Dashboard
1306
-
1307
- - **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.
1308
- - **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.
1309
-
1310
- ### Handling cancellation in workflows
1311
-
1312
- `conditionLT` returns `T | false | null`. Always guard before accessing the payload:
1313
-
1314
- ```typescript
1315
- const decision = await conditionLT<{ approved: boolean }>(signalId, escalationConfig);
1316
-
1317
- if (decision === null) {
1318
- // Escalation was cancelled (workflow terminated or explicit cancel)
1319
- return { type: 'return' as const, data: { cancelled: true } };
1320
- }
1321
- if (decision === false) {
1322
- // Escalation timed out
1323
- return { type: 'return' as const, data: { timedOut: true } };
1324
- }
1325
-
1326
- // Normal path — decision is the resolver's payload
1327
- ```
1328
-
1329
- The `!decision` shorthand handles both cases when you don't need to distinguish between them:
1330
-
1331
- ```typescript
1332
- if (!decision) {
1333
- return { type: 'return' as const, data: { cancelled: true } };
1334
- }
1335
- ```
1336
-
1337
- ---
1338
-
1339
- ## What Long-tail Provides (For Free)
1340
-
1341
- When you author a HITL-backed workflow, the platform handles:
1342
-
1343
- - **Escalation routing** — role-based, priority-ordered work queues
1344
- - **Claim/release** — soft-lock with TTL, prevents duplicate work
1345
- - **Real-time updates** — NATS/Socket.IO events push changes to the dashboard instantly
1346
- - **Form rendering** — JSON Schema to rich form controls, no frontend code needed
1347
- - **Side panel** — help, AI analysis, metadata, context, and raw-record views beside the form
1348
- - **Section state persistence** — collapsed sections remembered across navigation
1349
- - **Escalation chains** — users can re-route work to other roles
1350
- - **AI triage** — optional auto-resolution for common patterns
1351
- - **Signal routing** — 5 resolution paths (conditionLT, waitFor, triage, re-run, notification-only)
1352
- - **Credential security** — password fields use ephemeral tokens, never stored in plain text
1353
- - **Telemetry** — trace IDs link escalations to OpenTelemetry traces
1354
- - **Bulk operations** — bulk claim, assign, escalate, triage, and cancel for queue management
1355
- - **Cancellation** — cancel pending escalations from the API or dashboard; `conditionLT` returns `null` so workflows handle it cleanly
1356
-
1357
- You write the workflow and the schema. Everything else is provided.
120
+ | `readOnly` | field | Static display |
121
+ | `required` | schema | Fields that block submission when empty |
122
+ | `title` / `description` | both | Form section header / helper text |
123
+ | `minimum` / `maximum` | field | Static numeric bounds — enforced before submission |
124
+ | `exclusiveMinimum` / `exclusiveMaximum` | field | Exclusive numeric bounds |
125
+ | `minLength` / `maxLength` | field | Static string length bounds |
126
+ | `pattern` | field | Regexp guard enforced before submission |