@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.
- package/README.md +22 -93
- package/build/api/escalations/helpers.d.ts +27 -0
- package/build/api/escalations/helpers.js +34 -0
- package/build/api/escalations/list.d.ts +2 -0
- package/build/api/escalations/list.js +13 -3
- package/build/api/escalations/resolve.js +16 -4
- package/build/routes/escalations/list.js +2 -0
- package/build/services/escalation/crud.d.ts +1 -1
- package/build/services/escalation/crud.js +6 -2
- package/build/services/escalation/facet-sql.d.ts +8 -1
- package/build/services/escalation/facet-sql.js +29 -1
- package/build/types/facets.d.ts +9 -0
- package/dashboard/dist/assets/{AdminDashboard-DXFIKXO3.js → AdminDashboard-D7Amh-EJ.js} +2 -2
- package/dashboard/dist/assets/{AdminDashboard-DXFIKXO3.js.map → AdminDashboard-D7Amh-EJ.js.map} +1 -1
- package/dashboard/dist/assets/{AgentConfigPage-CQE1mfjk.js → AgentConfigPage-CW2udbSO.js} +7 -7
- package/dashboard/dist/assets/{AgentConfigPage-CQE1mfjk.js.map → AgentConfigPage-CW2udbSO.js.map} +1 -1
- package/dashboard/dist/assets/{AgentDetailPage-DM59ozP_.js → AgentDetailPage-Dy3Qvlfk.js} +3 -3
- package/dashboard/dist/assets/{AgentDetailPage-DM59ozP_.js.map → AgentDetailPage-Dy3Qvlfk.js.map} +1 -1
- package/dashboard/dist/assets/{AgentsPage-DSK2uXI5.js → AgentsPage-D7WePZ0o.js} +2 -2
- package/dashboard/dist/assets/{AgentsPage-DSK2uXI5.js.map → AgentsPage-D7WePZ0o.js.map} +1 -1
- package/dashboard/dist/assets/AvailableEscalationsPage-Dx51E6s_.js +2 -0
- package/dashboard/dist/assets/AvailableEscalationsPage-Dx51E6s_.js.map +1 -0
- package/dashboard/dist/assets/BotPicker-KebTxTDi.js +2 -0
- package/dashboard/dist/assets/BotPicker-KebTxTDi.js.map +1 -0
- package/dashboard/dist/assets/CapabilitiesPage-3VTLEPGO.js +2 -0
- package/dashboard/dist/assets/{CapabilitiesPage-DFQryGBj.js.map → CapabilitiesPage-3VTLEPGO.js.map} +1 -1
- package/dashboard/dist/assets/{CollapsibleSection-B7-d5O_x.js → CollapsibleSection-BYgeZz9M.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-B7-d5O_x.js.map → CollapsibleSection-BYgeZz9M.js.map} +1 -1
- package/dashboard/dist/assets/{ConfirmDeleteModal-D9_1b4MW.js → ConfirmDeleteModal-C0x0HcHX.js} +2 -2
- package/dashboard/dist/assets/{ConfirmDeleteModal-D9_1b4MW.js.map → ConfirmDeleteModal-C0x0HcHX.js.map} +1 -1
- package/dashboard/dist/assets/CountdownTimer-DFqEYmiF.js +2 -0
- package/dashboard/dist/assets/CountdownTimer-DFqEYmiF.js.map +1 -0
- package/dashboard/dist/assets/CredentialsPage-CaH3J3SE.js +2 -0
- package/dashboard/dist/assets/{CredentialsPage-BWVgxRpZ.js.map → CredentialsPage-CaH3J3SE.js.map} +1 -1
- package/dashboard/dist/assets/{CronLabel-DNnyNqXH.js → CronLabel-Bma_4AC2.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-DNnyNqXH.js.map → CronLabel-Bma_4AC2.js.map} +1 -1
- package/dashboard/dist/assets/{CustomDurationPicker-SNsQZL_U.js → CustomDurationPicker-BxyjEX27.js} +2 -2
- package/dashboard/dist/assets/{CustomDurationPicker-SNsQZL_U.js.map → CustomDurationPicker-BxyjEX27.js.map} +1 -1
- package/dashboard/dist/assets/{DropZone-C1TpkVQG.js → DropZone-zxH-9B8c.js} +2 -2
- package/dashboard/dist/assets/{DropZone-C1TpkVQG.js.map → DropZone-zxH-9B8c.js.map} +1 -1
- package/dashboard/dist/assets/{ElapsedCell-C4r0IvUt.js → ElapsedCell-Dr1cEwoi.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-C4r0IvUt.js.map → ElapsedCell-Dr1cEwoi.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationListSchemaPage-D8Ec8Zhj.js → EscalationListSchemaPage-CLdsR-hK.js} +3 -3
- package/dashboard/dist/assets/{EscalationListSchemaPage-D8Ec8Zhj.js.map → EscalationListSchemaPage-CLdsR-hK.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationSchemaPage-BmEv7qU2.js → EscalationSchemaPage-DnEnvolM.js} +3 -3
- package/dashboard/dist/assets/{EscalationSchemaPage-BmEv7qU2.js.map → EscalationSchemaPage-DnEnvolM.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationsOverview-DkIy025e.js → EscalationsOverview-BH8cxgLI.js} +2 -2
- package/dashboard/dist/assets/{EscalationsOverview-DkIy025e.js.map → EscalationsOverview-BH8cxgLI.js.map} +1 -1
- package/dashboard/dist/assets/{EventTable-Co4RYeKO.js → EventTable-p-xsfYYg.js} +2 -2
- package/dashboard/dist/assets/{EventTable-Co4RYeKO.js.map → EventTable-p-xsfYYg.js.map} +1 -1
- package/dashboard/dist/assets/FilterBar-B5Vk5tBF.js +2 -0
- package/dashboard/dist/assets/FilterBar-B5Vk5tBF.js.map +1 -0
- package/dashboard/dist/assets/{GraphInvokePage-b7ukMy0z.js → GraphInvokePage-Cf9c_LJ-.js} +2 -2
- package/dashboard/dist/assets/{GraphInvokePage-b7ukMy0z.js.map → GraphInvokePage-Cf9c_LJ-.js.map} +1 -1
- package/dashboard/dist/assets/HomePage-BdCtKXAV.js +2 -0
- package/dashboard/dist/assets/HomePage-BdCtKXAV.js.map +1 -0
- package/dashboard/dist/assets/ListToolbar-CDlol8XJ.js +2 -0
- package/dashboard/dist/assets/{ListToolbar-DDG6DXVF.js.map → ListToolbar-CDlol8XJ.js.map} +1 -1
- package/dashboard/dist/assets/{McpOverview-P2o3b3mi.js → McpOverview-ChZ36p9t.js} +2 -2
- package/dashboard/dist/assets/{McpOverview-P2o3b3mi.js.map → McpOverview-ChZ36p9t.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryDetailPage-C2CeBLb0.js → McpQueryDetailPage-X4j_jm3r.js} +2 -2
- package/dashboard/dist/assets/{McpQueryDetailPage-C2CeBLb0.js.map → McpQueryDetailPage-X4j_jm3r.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryPage-D_N-3lp0.js → McpQueryPage-C30HsQfZ.js} +2 -2
- package/dashboard/dist/assets/{McpQueryPage-D_N-3lp0.js.map → McpQueryPage-C30HsQfZ.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunDetailPage-uDw3MWR_.js → McpRunDetailPage-18d7Iqfy.js} +2 -2
- package/dashboard/dist/assets/{McpRunDetailPage-uDw3MWR_.js.map → McpRunDetailPage-18d7Iqfy.js.map} +1 -1
- package/dashboard/dist/assets/{McpRunsPage-CDTUxghb.js → McpRunsPage-BHZHCwMM.js} +2 -2
- package/dashboard/dist/assets/{McpRunsPage-CDTUxghb.js.map → McpRunsPage-BHZHCwMM.js.map} +1 -1
- package/dashboard/dist/assets/Modal-D4afnAjc.js +2 -0
- package/dashboard/dist/assets/Modal-D4afnAjc.js.map +1 -0
- package/dashboard/dist/assets/{NamespacePill--ikKtMvx.js → NamespacePill-BQNMfY0m.js} +2 -2
- package/dashboard/dist/assets/{NamespacePill--ikKtMvx.js.map → NamespacePill-BQNMfY0m.js.map} +1 -1
- package/dashboard/dist/assets/OperationsPage-GMojwL3q.js +2 -0
- package/dashboard/dist/assets/OperationsPage-GMojwL3q.js.map +1 -0
- package/dashboard/dist/assets/OperatorDashboard-BGJrxLMU.js +2 -0
- package/dashboard/dist/assets/{OperatorDashboard-CVRD8nQH.js.map → OperatorDashboard-BGJrxLMU.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeader-CiFhzGc0.js → PageHeader-BIY1YvKR.js} +2 -2
- package/dashboard/dist/assets/{PageHeader-CiFhzGc0.js.map → PageHeader-BIY1YvKR.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeaderWithStats-_zOnWAhq.js → PageHeaderWithStats-TUyglVW0.js} +2 -2
- package/dashboard/dist/assets/{PageHeaderWithStats-_zOnWAhq.js.map → PageHeaderWithStats-TUyglVW0.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessDetailPage-Dt6PAK04.js → ProcessDetailPage-Bcp3GKFo.js} +2 -2
- package/dashboard/dist/assets/{ProcessDetailPage-Dt6PAK04.js.map → ProcessDetailPage-Bcp3GKFo.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessesListPage-CMkjASgs.js → ProcessesListPage-DPrvuLoU.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-CMkjASgs.js.map → ProcessesListPage-DPrvuLoU.js.map} +1 -1
- package/dashboard/dist/assets/RoleDetailPage-Ws3umQKQ.js +8 -0
- package/dashboard/dist/assets/RoleDetailPage-Ws3umQKQ.js.map +1 -0
- package/dashboard/dist/assets/RolePill-4LO_CuO3.js +2 -0
- package/dashboard/dist/assets/RolePill-4LO_CuO3.js.map +1 -0
- package/dashboard/dist/assets/RolesPage-D3NNU92h.js +2 -0
- package/dashboard/dist/assets/{RolesPage-DtHIjDss.js.map → RolesPage-D3NNU92h.js.map} +1 -1
- package/dashboard/dist/assets/RunAsSelector-Z2Yunqdf.js +2 -0
- package/dashboard/dist/assets/{RunAsSelector-C198CpFs.js.map → RunAsSelector-Z2Yunqdf.js.map} +1 -1
- package/dashboard/dist/assets/{SlidePanel-BcWKelpn.js → SlidePanel-DK4sWXmp.js} +2 -2
- package/dashboard/dist/assets/{SlidePanel-BcWKelpn.js.map → SlidePanel-DK4sWXmp.js.map} +1 -1
- package/dashboard/dist/assets/StickyPagination-CeLsnCIS.js +2 -0
- package/dashboard/dist/assets/{StickyPagination-eZ6WhMrL.js.map → StickyPagination-CeLsnCIS.js.map} +1 -1
- package/dashboard/dist/assets/{StreamMessageDetail-7xq_dddO.js → StreamMessageDetail-DWs7XkmH.js} +2 -2
- package/dashboard/dist/assets/{StreamMessageDetail-7xq_dddO.js.map → StreamMessageDetail-DWs7XkmH.js.map} +1 -1
- package/dashboard/dist/assets/{SwimlaneTimeline-Yg5QT0nI.js → SwimlaneTimeline-Dxl9a51q.js} +2 -2
- package/dashboard/dist/assets/{SwimlaneTimeline-Yg5QT0nI.js.map → SwimlaneTimeline-Dxl9a51q.js.map} +1 -1
- package/dashboard/dist/assets/{TagInput-BaFJCq9A.js → TagInput-CK-JhBG2.js} +2 -2
- package/dashboard/dist/assets/{TagInput-BaFJCq9A.js.map → TagInput-CK-JhBG2.js.map} +1 -1
- package/dashboard/dist/assets/{TaskDetailPage-Wc3AFnqs.js → TaskDetailPage-DVr31zDE.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-Wc3AFnqs.js.map → TaskDetailPage-DVr31zDE.js.map} +1 -1
- package/dashboard/dist/assets/{TaskQueuePill-D9e16qkU.js → TaskQueuePill-C_Vo2Clc.js} +2 -2
- package/dashboard/dist/assets/{TaskQueuePill-D9e16qkU.js.map → TaskQueuePill-C_Vo2Clc.js.map} +1 -1
- package/dashboard/dist/assets/{TasksListPage-V-1J_B_L.js → TasksListPage-DNGp5lfi.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-V-1J_B_L.js.map → TasksListPage-DNGp5lfi.js.map} +1 -1
- package/dashboard/dist/assets/{TimeAgo-XdHghs5F.js → TimeAgo-j2Q4w4lY.js} +2 -2
- package/dashboard/dist/assets/{TimeAgo-XdHghs5F.js.map → TimeAgo-j2Q4w4lY.js.map} +1 -1
- package/dashboard/dist/assets/{TimestampCell-BrzctPWT.js → TimestampCell-BLEfob2C.js} +2 -2
- package/dashboard/dist/assets/{TimestampCell-BrzctPWT.js.map → TimestampCell-BLEfob2C.js.map} +1 -1
- package/dashboard/dist/assets/ToolPill-BCi101Jh.js +2 -0
- package/dashboard/dist/assets/{ToolPill-Enc3nyic.js.map → ToolPill-BCi101Jh.js.map} +1 -1
- package/dashboard/dist/assets/{ToolTestPanel-CGfGUsaq.js → ToolTestPanel-ZJZT_mDP.js} +2 -2
- package/dashboard/dist/assets/{ToolTestPanel-CGfGUsaq.js.map → ToolTestPanel-ZJZT_mDP.js.map} +1 -1
- package/dashboard/dist/assets/{TopicDetailPage-CcAgD2k5.js → TopicDetailPage-ynUkO1A8.js} +3 -3
- package/dashboard/dist/assets/{TopicDetailPage-CcAgD2k5.js.map → TopicDetailPage-ynUkO1A8.js.map} +1 -1
- package/dashboard/dist/assets/{TopicsPage-BOQG7s4P.js → TopicsPage-Ctb_85Tw.js} +2 -2
- package/dashboard/dist/assets/{TopicsPage-BOQG7s4P.js.map → TopicsPage-Ctb_85Tw.js.map} +1 -1
- package/dashboard/dist/assets/{UserName-BzxR0Ihp.js → UserName-CkFpUqVK.js} +2 -2
- package/dashboard/dist/assets/{UserName-BzxR0Ihp.js.map → UserName-CkFpUqVK.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowExecutionPage-hRsDMByj.js → WorkflowExecutionPage-CSw2aQJC.js} +2 -2
- package/dashboard/dist/assets/{WorkflowExecutionPage-hRsDMByj.js.map → WorkflowExecutionPage-CSw2aQJC.js.map} +1 -1
- package/dashboard/dist/assets/WorkflowPill-DHxrAkp3.js +2 -0
- package/dashboard/dist/assets/{WorkflowPill-S6VV2Eax.js.map → WorkflowPill-DHxrAkp3.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsDashboard-ky0ulsUe.js → WorkflowsDashboard-DOyZbqzf.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsDashboard-ky0ulsUe.js.map → WorkflowsDashboard-DOyZbqzf.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsOverview-CzRJUV58.js → WorkflowsOverview-CRyHvUBx.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsOverview-CzRJUV58.js.map → WorkflowsOverview-CRyHvUBx.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-Beqzy9Ph.js → YamlWorkflowDetailPage-DRBmQXY6.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-Beqzy9Ph.js.map → YamlWorkflowDetailPage-DRBmQXY6.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowsPage-BzBUU2ug.js → YamlWorkflowsPage-HnI-9eBW.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowsPage-BzBUU2ug.js.map → YamlWorkflowsPage-HnI-9eBW.js.map} +1 -1
- package/dashboard/dist/assets/{agents-Dlu4m6jy.js → agents-BA2CvQO9.js} +2 -2
- package/dashboard/dist/assets/{agents-Dlu4m6jy.js.map → agents-BA2CvQO9.js.map} +1 -1
- package/dashboard/dist/assets/{bots-C4G5U-6Q.js → bots-DHHnoo8x.js} +2 -2
- package/dashboard/dist/assets/{bots-C4G5U-6Q.js.map → bots-DHHnoo8x.js.map} +1 -1
- package/dashboard/dist/assets/{capabilities-BSFP_boq.js → capabilities-BYBKLRRI.js} +2 -2
- package/dashboard/dist/assets/{capabilities-BSFP_boq.js.map → capabilities-BYBKLRRI.js.map} +1 -1
- package/dashboard/dist/assets/{controlplane-qCqoP2ZV.js → controlplane-H0Ljhb-1.js} +2 -2
- package/dashboard/dist/assets/{controlplane-qCqoP2ZV.js.map → controlplane-H0Ljhb-1.js.map} +1 -1
- package/dashboard/dist/assets/escalation-columns-BV3Ztn7D.js +2 -0
- package/dashboard/dist/assets/escalation-columns-BV3Ztn7D.js.map +1 -0
- package/dashboard/dist/assets/index-B4FaugP1.js +2 -0
- package/dashboard/dist/assets/{index-BtEWQNcP.js.map → index-B4FaugP1.js.map} +1 -1
- package/dashboard/dist/assets/{index-D_FNaSoe.js → index-BI-9M7z3.js} +2 -2
- package/dashboard/dist/assets/{index-D_FNaSoe.js.map → index-BI-9M7z3.js.map} +1 -1
- package/dashboard/dist/assets/{index-CuC_mNNA.js → index-BXrm76Hm.js} +2 -2
- package/dashboard/dist/assets/{index-CuC_mNNA.js.map → index-BXrm76Hm.js.map} +1 -1
- package/dashboard/dist/assets/index-BeFjLCIQ.js +5 -0
- package/dashboard/dist/assets/index-BeFjLCIQ.js.map +1 -0
- package/dashboard/dist/assets/index-BrEcQQ1p.css +1 -0
- package/dashboard/dist/assets/{index-BObWYPxd.js → index-CP6D_Gtw.js} +2 -2
- package/dashboard/dist/assets/{index-BObWYPxd.js.map → index-CP6D_Gtw.js.map} +1 -1
- package/dashboard/dist/assets/index-CVMA5PVQ.js +2 -0
- package/dashboard/dist/assets/{index-C0KklOM4.js.map → index-CVMA5PVQ.js.map} +1 -1
- package/dashboard/dist/assets/{index-CGVqW6MI.js → index-CWkAN6wc.js} +2 -2
- package/dashboard/dist/assets/{index-CGVqW6MI.js.map → index-CWkAN6wc.js.map} +1 -1
- package/dashboard/dist/assets/index-D1Q8WfGN.js +2 -0
- package/dashboard/dist/assets/{index-QL26JxBF.js.map → index-D1Q8WfGN.js.map} +1 -1
- package/dashboard/dist/assets/{index-DUhettdV.js → index-DSjih1iX.js} +2 -2
- package/dashboard/dist/assets/{index-DUhettdV.js.map → index-DSjih1iX.js.map} +1 -1
- package/dashboard/dist/assets/{index-Dzhn5MDY.js → index-Dx6-4esw.js} +2 -2
- package/dashboard/dist/assets/{index-Dzhn5MDY.js.map → index-Dx6-4esw.js.map} +1 -1
- package/dashboard/dist/assets/index-O2wWcCLo.js +5 -0
- package/dashboard/dist/assets/{index-DWSNtg28.js.map → index-O2wWcCLo.js.map} +1 -1
- package/dashboard/dist/assets/{index-DCTWUswT.js → index-YWvrMcqh.js} +4 -4
- package/dashboard/dist/assets/index-YWvrMcqh.js.map +1 -0
- package/dashboard/dist/assets/index-iX8HLLB9.js +63 -0
- package/dashboard/dist/assets/index-iX8HLLB9.js.map +1 -0
- package/dashboard/dist/assets/{knowledge-CTdgLX5j.js → knowledge-BVbElRHf.js} +2 -2
- package/dashboard/dist/assets/{knowledge-CTdgLX5j.js.map → knowledge-BVbElRHf.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-BEsMr678.js → mcp-CstDdowJ.js} +2 -2
- package/dashboard/dist/assets/{mcp-BEsMr678.js.map → mcp-CstDdowJ.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-CmamHKIR.js → mcp-query-DHUs0N8H.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-CmamHKIR.js.map → mcp-query-DHUs0N8H.js.map} +1 -1
- package/dashboard/dist/assets/{pipelines-taOXA2-y.js → pipelines-BDmAzE3U.js} +2 -2
- package/dashboard/dist/assets/{pipelines-taOXA2-y.js.map → pipelines-BDmAzE3U.js.map} +1 -1
- package/dashboard/dist/assets/{tasks-DEaDdyBh.js → tasks--qZocuG-.js} +2 -2
- package/dashboard/dist/assets/{tasks-DEaDdyBh.js.map → tasks--qZocuG-.js.map} +1 -1
- package/dashboard/dist/assets/{topics-2qRx68Ft.js → topics-P3jkjMsx.js} +2 -2
- package/dashboard/dist/assets/{topics-2qRx68Ft.js.map → topics-P3jkjMsx.js.map} +1 -1
- package/dashboard/dist/assets/{useEventHooks-oXjfKrbI.js → useEventHooks-CqH8Pasj.js} +2 -2
- package/dashboard/dist/assets/{useEventHooks-oXjfKrbI.js.map → useEventHooks-CqH8Pasj.js.map} +1 -1
- package/dashboard/dist/assets/{useNamespace-YdT1Zouk.js → useNamespace-aI3tPfo3.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-YdT1Zouk.js.map → useNamespace-aI3tPfo3.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-Bt-RZ7Zl.js → useYamlActivityEvents-CPVyuW8i.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-Bt-RZ7Zl.js.map → useYamlActivityEvents-CPVyuW8i.js.map} +1 -1
- package/dashboard/dist/assets/{users-DbX5JZ_w.js → users-BrcmIH7O.js} +2 -2
- package/dashboard/dist/assets/{users-DbX5JZ_w.js.map → users-BrcmIH7O.js.map} +1 -1
- package/dashboard/dist/assets/{vendor-icons-DPmIjhZB.js → vendor-icons-Qk9qLjF_.js} +131 -126
- package/dashboard/dist/assets/vendor-icons-Qk9qLjF_.js.map +1 -0
- package/dashboard/dist/assets/{workflows-acMcPTC5.js → workflows-DPhX5L5C.js} +2 -2
- package/dashboard/dist/assets/{workflows-acMcPTC5.js.map → workflows-DPhX5L5C.js.map} +1 -1
- package/dashboard/dist/assets/{yaml-workflows-D6kzrMYx.js → yaml-workflows-KZI2cufF.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-D6kzrMYx.js.map → yaml-workflows-KZI2cufF.js.map} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/docs/dashboard.md +2 -2
- package/docs/faceted-routing.md +9 -0
- package/docs/hitl/escalation.md +206 -0
- package/docs/hitl/form.md +162 -0
- package/docs/hitl/iframe.md +170 -0
- package/docs/hitl/resolution.md +166 -0
- package/docs/hitl/roles.md +72 -0
- package/docs/hitl/x-lt-layout.md +167 -0
- package/docs/hitl/x-lt-list-schema.md +75 -0
- package/docs/hitl/x-lt-show-if.md +128 -0
- package/docs/hitl/x-lt-validation.md +195 -0
- package/docs/hitl/x-lt-widget.md +162 -0
- package/docs/hitl-guide.md +84 -1315
- package/docs/iam.md +1 -1
- package/package.json +2 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-BVgMtftp.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-BVgMtftp.js.map +0 -1
- package/dashboard/dist/assets/BotPicker-DJ0YYKZA.js +0 -2
- package/dashboard/dist/assets/BotPicker-DJ0YYKZA.js.map +0 -1
- package/dashboard/dist/assets/CapabilitiesPage-DFQryGBj.js +0 -2
- package/dashboard/dist/assets/CountdownTimer-BTJmh4Xw.js +0 -2
- package/dashboard/dist/assets/CountdownTimer-BTJmh4Xw.js.map +0 -1
- package/dashboard/dist/assets/CredentialsPage-BWVgxRpZ.js +0 -2
- package/dashboard/dist/assets/FilterBar-DDIn8TOs.js +0 -2
- package/dashboard/dist/assets/FilterBar-DDIn8TOs.js.map +0 -1
- package/dashboard/dist/assets/HomePage-K0U3yMdU.js +0 -2
- package/dashboard/dist/assets/HomePage-K0U3yMdU.js.map +0 -1
- package/dashboard/dist/assets/ListToolbar-DDG6DXVF.js +0 -2
- package/dashboard/dist/assets/Modal-CSrxpXeM.js +0 -2
- package/dashboard/dist/assets/Modal-CSrxpXeM.js.map +0 -1
- package/dashboard/dist/assets/OperationsPage-Daah4pNk.js +0 -2
- package/dashboard/dist/assets/OperationsPage-Daah4pNk.js.map +0 -1
- package/dashboard/dist/assets/OperatorDashboard-CVRD8nQH.js +0 -2
- package/dashboard/dist/assets/RoleDetailPage-6H1AY6sK.js +0 -8
- package/dashboard/dist/assets/RoleDetailPage-6H1AY6sK.js.map +0 -1
- package/dashboard/dist/assets/RolePill-CUIJSgRq.js +0 -2
- package/dashboard/dist/assets/RolePill-CUIJSgRq.js.map +0 -1
- package/dashboard/dist/assets/RolesPage-DtHIjDss.js +0 -2
- package/dashboard/dist/assets/RunAsSelector-C198CpFs.js +0 -2
- package/dashboard/dist/assets/StickyPagination-eZ6WhMrL.js +0 -2
- package/dashboard/dist/assets/ToolPill-Enc3nyic.js +0 -2
- package/dashboard/dist/assets/WorkflowPill-S6VV2Eax.js +0 -2
- package/dashboard/dist/assets/escalation-columns-Ck7ZOd7Z.js +0 -2
- package/dashboard/dist/assets/escalation-columns-Ck7ZOd7Z.js.map +0 -1
- package/dashboard/dist/assets/index-BtEWQNcP.js +0 -2
- package/dashboard/dist/assets/index-C0KklOM4.js +0 -2
- package/dashboard/dist/assets/index-CY3hpJR-.js +0 -5
- package/dashboard/dist/assets/index-CY3hpJR-.js.map +0 -1
- package/dashboard/dist/assets/index-DCTWUswT.js.map +0 -1
- package/dashboard/dist/assets/index-DWSNtg28.js +0 -5
- package/dashboard/dist/assets/index-DcQiD6OF.js +0 -63
- package/dashboard/dist/assets/index-DcQiD6OF.js.map +0 -1
- package/dashboard/dist/assets/index-QL26JxBF.js +0 -2
- package/dashboard/dist/assets/index-waKF7bKZ.css +0 -1
- package/dashboard/dist/assets/roles-BiUJAsVM.js +0 -2
- package/dashboard/dist/assets/roles-BiUJAsVM.js.map +0 -1
- package/dashboard/dist/assets/vendor-icons-DPmIjhZB.js.map +0 -1
package/docs/hitl-guide.md
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
# Human-in-the-Loop (HITL)
|
|
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
|
|
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
|
-
##
|
|
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
|
|
24
|
-
3. **
|
|
25
|
-
4. **
|
|
26
|
-
5. **Workflow resumes** — continues execution with the
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
You write the workflow and the schema. Everything else is provided.
|
|
203
70
|
|
|
204
71
|
---
|
|
205
72
|
|
|
206
|
-
##
|
|
73
|
+
## Section Map
|
|
207
74
|
|
|
208
|
-
|
|
75
|
+
Ordered as a learning path — each file adds one capability to the same form:
|
|
209
76
|
|
|
210
|
-
|
|
|
211
|
-
|
|
212
|
-
|
|
|
213
|
-
|
|
|
214
|
-
|
|
|
215
|
-
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
##
|
|
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"`
|
|
250
|
-
| `x-lt-
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
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
|
|
255
|
-
| `x-lt-hide-if-empty` | field | `true` — suppress the field
|
|
256
|
-
| `x-lt-section` | field |
|
|
257
|
-
| `x-lt-
|
|
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
|
|
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
|
|
265
|
-
| `required` | schema | Fields that
|
|
266
|
-
| `title` / `description` | both |
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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 |
|