LlamaIndex Tutorial (TypeScript): adding human-in-the-loop for intermediate developers
This tutorial shows how to pause a LlamaIndex TypeScript workflow, send a tool decision to a human for review, and resume execution with the approved result. You need this when an agent is allowed to act on risky requests like sending emails, updating records, or making policy decisions, but you still want a person in the loop before anything is committed.
What You'll Need
- •Node.js 18+ and npm
- •A TypeScript project with
ts-nodeortsx - •
@llamaindex/core - •
dotenvfor environment variables - •An OpenAI API key if you want to use an LLM-backed workflow
- •A terminal where you can run a local approval prompt
- •Basic familiarity with LlamaIndex workflows and async/await
Install the packages:
npm install @llamaindex/core dotenv
npm install -D typescript tsx @types/node
Step-by-Step
- •Start by defining a workflow that asks a human before executing a sensitive action. The pattern here is simple: the agent proposes an action, your app pauses, and a human approves or rejects it.
import { Workflow, step } from "@llamaindex/core/workflow";
type ApprovalRequest = {
action: string;
target: string;
};
type ApprovalResult = {
approved: boolean;
reviewer: string;
};
export class HumanApprovalWorkflow extends Workflow {
@step()
async propose(request: ApprovalRequest): Promise<ApprovalResult> {
console.log(`Proposed action: ${request.action} -> ${request.target}`);
// In production, replace this with Slack, Jira, email, or an internal review UI.
const approved = false;
return {
approved,
reviewer: "human-reviewer",
};
}
}
- •Next, wire in an actual human checkpoint. For a terminal-based version, use Node’s built-in
readline/promisesso the workflow can wait for approval before continuing.
import { createInterface } from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
export async function askHuman(question: string): Promise<boolean> {
const rl = createInterface({ input, output });
const answer = await rl.question(`${question} (yes/no): `);
rl.close();
return answer.trim().toLowerCase() === "yes";
}
- •Now connect that checkpoint to your workflow step. If the human rejects the request, stop immediately; if approved, continue with the downstream action.
import { Workflow, step } from "@llamaindex/core/workflow";
import { askHuman } from "./approval";
type ApprovalRequest = {
action: string;
target: string;
};
export class HumanApprovalWorkflow extends Workflow {
@step()
async propose(request: ApprovalRequest): Promise<string> {
const approved = await askHuman(
`Approve ${request.action} for ${request.target}?`
);
if (!approved) {
throw new Error("Human rejected the request");
}
return `Approved ${request.action} for ${request.target}`;
}
}
- •Add a second step for the actual business action. This keeps approval logic separate from execution logic, which is what you want in production systems where auditability matters.
import { Workflow, step } from "@llamaindex/core/workflow";
import { askHuman } from "./approval";
type ApprovalRequest = {
action: string;
target: string;
};
export class HumanApprovalWorkflow extends Workflow {
@step()
async approve(request: ApprovalRequest): Promise<ApprovalRequest> {
const approved = await askHuman(
`Approve ${request.action} for ${request.target}?`
);
if (!approved) throw new Error("Rejected by human");
return request;
}
@step()
async execute(request: ApprovalRequest): Promise<string> {
return `Executed ${request.action} on ${request.target}`;
}
}
- •Finally, run the workflow from a small entrypoint. This gives you a concrete end-to-end flow you can adapt into an API route, background job, or agent tool handler.
import "dotenv/config";
import { HumanApprovalWorkflow } from "./workflow";
async function main() {
const workflow = new HumanApprovalWorkflow();
const result = await workflow.run({
action: "send_email",
target: "customer@example.com",
});
console.log(result);
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});
Testing It
Run the script and watch it pause for approval in your terminal. Type yes to continue or anything else to reject the request.
A successful run should print the final execution message after approval. A rejected run should fail fast with Rejected by human, which is what you want for sensitive operations.
To make sure it behaves correctly in real usage, test both paths:
- •Approved request returns an execution result
- •Rejected request throws before execution
- •Empty input is treated as rejection
Next Steps
- •Replace terminal approval with Slack or Microsoft Teams using a webhook callback
- •Add audit logging so every approval stores requester, reviewer, timestamp, and payload
- •Wrap this pattern around tool calls like email sending, CRM updates, or payment actions
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit