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