@5dive/langgraph-telegram-hitl
v0.1.0
Published
Human-in-the-loop for LangGraph.js over Telegram — surface interrupt() payloads as approve/reject/edit buttons in a Telegram chat and feed the human's answer back into the graph via Command(resume).
Downloads
33
Maintainers
Readme
@5dive/langgraph-telegram-hitl
Human-in-the-loop for LangGraph.js over Telegram.
LangGraph has native HITL — interrupt() / Command(resume=...) (with a checkpointer) — and an official Slack approval integration, but no Telegram equivalent. This package fills that gap: when your graph hits an interrupt(), the payload is pushed to a Telegram chat as an Approve / Edit / Respond / Reject button prompt. The human taps (or replies with text), and the answer is fed straight back into the graph via Command(resume=...).
- Zero build step — plain ESM, runs on Node ≥ 18 (uses global
fetch). - Transport is injectable, so the whole flow is unit-testable without a live bot.
- Interrupt/response shapes follow LangGraph's
HumanInterrupt/HumanResponseconventions, so Agent-Inbox-style graphs work unchanged.
Install
npm install @5dive/langgraph-telegram-hitl @langchain/langgraphQuickstart
Your graph interrupts like any LangGraph HITL graph (a checkpointer is required):
import { StateGraph, MessagesAnnotation, MemorySaver, interrupt } from "@langchain/langgraph";
import { TelegramHITL } from "@5dive/langgraph-telegram-hitl";
const graph = new StateGraph(MessagesAnnotation)
.addNode("askHuman", async (state) => {
// Pause and ask a human to approve the pending action.
const decision = interrupt({
action_request: { action: "send_email", args: { to: "[email protected]", subject: "Q3" } },
description: "Send this email?",
config: { allow_accept: true, allow_edit: true, allow_ignore: true },
});
// `decision` is the HumanResponse the driver resumes with.
return { messages: [{ role: "user", content: JSON.stringify(decision) }] };
})
.addEdge("__start__", "askHuman")
.addEdge("askHuman", "__end__")
.compile({ checkpointer: new MemorySaver() });
const hitl = new TelegramHITL({
token: process.env.TELEGRAM_BOT_TOKEN,
chatId: process.env.TELEGRAM_CHAT_ID,
});
// Runs the graph to completion, brokering every interrupt through Telegram.
const finalState = await hitl.run(graph, { messages: [] }, {
configurable: { thread_id: "email-review-1" },
});When the graph interrupts, the chat receives:
🔔 Human input needed
Action: send_email
{ "to": "[email protected]", "subject": "Q3" }
Send this email?
[ ✅ Approve ] [ ✏️ Edit ] [ 🚫 Reject ]- Approve → resumes with
{ type: "accept", args: null } - Reject → resumes with
{ type: "ignore", args: null } - Edit / Respond → the bot asks for text; your next chat message becomes
{ type: "edit"|"response", args: "<your text>" }
API
new TelegramHITL(options)
| option | type | notes |
| --- | --- | --- |
| token | string | Telegram bot token (omit if you pass a custom transport) |
| chatId | string | number | required — the chat that approves |
| transport | object | custom transport; defaults to the built-in fetch client |
| commandFactory | (resume) => Command | defaults to new Command({ resume }) from @langchain/langgraph |
| pollTimeout | number | getUpdates long-poll seconds (default 30) |
| onUpdate | (chunk) => void | called with every raw graph stream chunk |
hitl.run(graph, input, config)
Streams the compiled graph, handling each __interrupt__ over Telegram and resuming until the graph completes. A thread_id is generated into config.configurable if absent. Returns the graph's final state (graph.getState).
Interrupt payload → buttons
The payload's config flags decide which buttons show. With no config, all four are offered:
| flag | button | resume type |
| --- | --- | --- |
| allow_accept | ✅ Approve | accept |
| allow_edit | ✏️ Edit | edit (+ text) |
| allow_respond | 💬 Respond | response (+ text) |
| allow_ignore | 🚫 Reject | ignore |
A bare string or object interrupt value is rendered as text with all buttons.
Helpers (exported for custom UIs)
describeInterrupt(value), allowedActions(value), buildKeyboard(value, token), parseCallback(data, token), TelegramTransport.
Testing your own graphs
Inject a fake transport (records sends, serves scripted getUpdates batches) and a plain commandFactory to drive the whole loop with no network. See test/run.test.js.
License
MIT © 5dive
