Skip to Content
DocumentationUI/UXHITL Approval Node

HITL Approval Node

In this section we will build a simple UI node that accepts an OTP and continues the cascade. This is part of the booking agent in our tutorial.

export default function BookingUi(nodeId: string) { const { addActiveNode, signalCompletion, nodeData } = useWorkflow(nodeId); const assistantmessage = nodeData.initialContext.history.assistantMessage; const cascadeId = nodeData.initialContext.cascadeId; const [pin, setPin] = useState(""); const handleSubmit = async (e) => { e.preventDefault(); const toolResponse = { role: "tool", tool_call_id: nodeData.initialContext.history.tool_calls[0].id, content: "Booking confirmed, booking id is : azzdfgr146", }; await addActiveNode("bookingAgentNode", { cascadeId: cascadeId, history: [toolResponse], userId: "guest-id", }); signalCompletion({ nodeId, hasSpawns: true }); }; return ( <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"> <div className="w-full max-w-sm p-8 bg-white rounded-2xl shadow-2xl transform transition-all scale-100"> <h2 className="text-2xl font-bold text-center text-gray-800 mb-6"> Enter PIN </h2> <form onSubmit={handleSubmit} className="space-y-4"> <div> <input type="password" inputMode="numeric" maxLength={6} value={pin} onChange={(e) => setPin(e.target.value.replace(/\D/g, ""))} placeholder="••••••" className="w-full px-4 py-3 text-center text-2xl tracking-widest border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" required autoFocus /> </div> <button type="submit" className="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-md transition-colors duration-200" > Submit </button> </form> <p className="mt-4 text-xs text-center text-gray-500"> Please enter your security code to continue. </p> </div> </div> ); }

Note: look at how the outmost div is structured, it places this UI in a fixed position above the other active UI nodes. This is how you take advantage of the workflowRenderer to create custom UI arrangements easily.

The lifecycle is:

  • get the required data from nodeData
  • accept input from user
  • activate the next node using addActiveNode and the formatted user input
  • signal completion to remove the node

The above case was for Cascaide. In Cascaide Lite, the behaviour changes slightly. The rule is

  • UI nodes will recieve the entire history of the cascade from as a part of nodeData
  • When the next node is activated, the history passed in should contain the full cascade history
const {addActiveNode, signalCompletion, nodeData} = useWorkflow(nodeId); const cascadeId = nodeData.initialContext.cascadeId; const [pin, setPin] = useState(''); const handleSubmit = async (e) => { e.preventDefault(); const toolResponse = { "role":"tool", "tool_call_id": nodeData.initialContext.history[nodeData.initialContext.history.length-1].tool_calls[0].id, "content": "Booking confirmed, booking id is : azzdfgr146" } const updatedHistory = [...nodeData.initialContext.history, toolResponse ] await addActiveNode('bookingAgentNode', { cascadeId: cascadeId, history: updatedHistory, userId: "guest-id" }); signalCompletion({nodeId,hasSpawns:true}); };
Last updated on