[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6C5HBQTM2Q4FAHC06SZGCVW":3,"skill-same:01M6C5HBQTM2Q4FAHC06SZGCVW":66},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":35,"linkOnly":36,"markdownContent":37,"markdownBody":38,"frontMatter":39,"refName":40,"commitSha":41,"skillDirectory":42,"forksCount":14,"repositoryHtmlUrl":43,"repositoryHomepageUrl":6,"ownerHtmlUrl":44,"ownerBlogUrl":45,"ownerDescription":6,"updatedAt":46,"install":47,"resources":60},"01M6C5HBQTM2Q4FAHC06SZGCVW","langgraph-human-in-the-loop",null,"INVOKE THIS SKILL when implementing human-in-the-loop patterns, pausing for approval, or handling errors in LangGraph. Covers interrupt(), Command(resume=...), approval\u002Fvalidation workflows, and the 4-tier error handling strategy.","CoralShades\u002Facm-ai","CoralShades","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F220297585?v=4","01M6C5211QSJHZ8HXSDDWCR507","Coral Shades AI Agency","01M6C52161FNQDRQ0FRR8DAA9D",0,"MIT","https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Flanggraph-human-in-the-loop\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000038","人力资源","PROFESSION","适用职业",[24,25,26,27,28,29,30,31,32,33,34],"langgraph","loop","langgraph human in the loop","workflows","error handling","validation","human-in-the-loop","command","strategy","approval","resume","2026-11-10 10:12:30",false,"---\nname: langgraph-human-in-the-loop\ndescription: \"INVOKE THIS SKILL when implementing human-in-the-loop patterns, pausing for approval, or handling errors in LangGraph. Covers interrupt(), Command(resume=...), approval\u002Fvalidation workflows, and the 4-tier error handling strategy.\"\n---\n\n\u003Coverview>\nLangGraph's human-in-the-loop patterns let you pause graph execution, surface data to users, and resume with their input:\n\n- **`interrupt(value)`** — pauses execution, surfaces a value to the caller\n- **`Command(resume=value)`** — resumes execution, providing the value back to `interrupt()`\n- **Checkpointer** — required to save state while paused\n- **Thread ID** — required to identify which paused execution to resume\n\u003C\u002Foverview>\n\n---\n\n## Requirements\n\nThree things are required for interrupts to work:\n\n1. **Checkpointer** — compile with `checkpointer=InMemorySaver()` (dev) or `PostgresSaver` (prod)\n2. **Thread ID** — pass `{\"configurable\": {\"thread_id\": \"...\"}}` to every `invoke`\u002F`stream` call\n3. **JSON-serializable payload** — the value passed to `interrupt()` must be JSON-serializable\n\n---\n\n## Basic Interrupt + Resume\n\n`interrupt(value)` pauses the graph. The value surfaces in the result under `__interrupt__`. `Command(resume=value)` resumes — the resume value becomes the return value of `interrupt()`.\n\n**Critical**: when the graph resumes, the node restarts from the **beginning** — all code before `interrupt()` re-runs.\n\n\u003Cex-basic-interrupt-resume>\n\u003Cpython>\nPause execution for human review and resume with Command.\n```python\nfrom langgraph.types import interrupt, Command\nfrom langgraph.checkpoint.memory import InMemorySaver\nfrom langgraph.graph import StateGraph, START, END\nfrom typing_extensions import TypedDict\n\nclass State(TypedDict):\n    approved: bool\n\ndef approval_node(state: State):\n    # Pause and ask for approval\n    approved = interrupt(\"Do you approve this action?\")\n    # When resumed, Command(resume=...) returns that value here\n    return {\"approved\": approved}\n\ncheckpointer = InMemorySaver()\ngraph = (\n    StateGraph(State)\n    .add_node(\"approval\", approval_node)\n    .add_edge(START, \"approval\")\n    .add_edge(\"approval\", END)\n    .compile(checkpointer=checkpointer)\n)\n\nconfig = {\"configurable\": {\"thread_id\": \"thread-1\"}}\n\n# Initial run — hits interrupt and pauses\nresult = graph.invoke({\"approved\": False}, config)\nprint(result[\"__interrupt__\"])\n# [Interrupt(value='Do you approve this action?')]\n\n# Resume with the human's response\nresult = graph.invoke(Command(resume=True), config)\nprint(result[\"approved\"])  # True\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nPause execution for human review and resume with Command.\n```typescript\nimport { interrupt, Command, MemorySaver, StateGraph, StateSchema, START, END } from \"@langchain\u002Flanggraph\";\nimport { z } from \"zod\";\n\nconst State = new StateSchema({\n  approved: z.boolean().default(false),\n});\n\nconst approvalNode = async (state: typeof State.State) => {\n  \u002F\u002F Pause and ask for approval\n  const approved = interrupt(\"Do you approve this action?\");\n  \u002F\u002F When resumed, Command({ resume }) returns that value here\n  return { approved };\n};\n\nconst checkpointer = new MemorySaver();\nconst graph = new StateGraph(State)\n  .addNode(\"approval\", approvalNode)\n  .addEdge(START, \"approval\")\n  .addEdge(\"approval\", END)\n  .compile({ checkpointer });\n\nconst config = { configurable: { thread_id: \"thread-1\" } };\n\n\u002F\u002F Initial run — hits interrupt and pauses\nlet result = await graph.invoke({ approved: false }, config);\nconsole.log(result.__interrupt__);\n\u002F\u002F [{ value: 'Do you approve this action?', ... }]\n\n\u002F\u002F Resume with the human's response\nresult = await graph.invoke(new Command({ resume: true }), config);\nconsole.log(result.approved);  \u002F\u002F true\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-basic-interrupt-resume>\n\n---\n\n## Approval Workflow\n\nA common pattern: interrupt to show a draft, then route based on the human's decision.\n\n\u003Cex-approval-workflow>\n\u003Cpython>\nInterrupt for human review, then route to send or end based on the decision.\n```python\nfrom langgraph.types import interrupt, Command\nfrom langgraph.graph import StateGraph, START, END\nfrom typing import Literal\nfrom typing_extensions import TypedDict\n\nclass EmailAgentState(TypedDict):\n    email_content: str\n    draft_response: str\n    classification: dict\n\ndef human_review(state: EmailAgentState) -> Command[Literal[\"send_reply\", \"__end__\"]]:\n    \"\"\"Pause for human review using interrupt and route based on decision.\"\"\"\n    classification = state.get(\"classification\", {})\n\n    # interrupt() must come first — any code before it will re-run on resume\n    human_decision = interrupt({\n        \"email_id\": state.get(\"email_content\", \"\"),\n        \"draft_response\": state.get(\"draft_response\", \"\"),\n        \"urgency\": classification.get(\"urgency\"),\n        \"action\": \"Please review and approve\u002Fedit this response\"\n    })\n\n    # Process the human's decision\n    if human_decision.get(\"approved\"):\n        return Command(\n            update={\"draft_response\": human_decision.get(\"edited_response\", state.get(\"draft_response\", \"\"))},\n            goto=\"send_reply\"\n        )\n    else:\n        # Rejection — human will handle directly\n        return Command(update={}, goto=END)\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nInterrupt for human review, then route to send or end based on the decision.\n```typescript\nimport { interrupt, Command, END, GraphNode } from \"@langchain\u002Flanggraph\";\n\nconst humanReview: GraphNode\u003Ctypeof EmailAgentState> = async (state) => {\n  const classification = state.classification!;\n\n  \u002F\u002F interrupt() must come first — any code before it will re-run on resume\n  const humanDecision = interrupt({\n    emailId: state.emailContent,\n    draftResponse: state.responseText,\n    urgency: classification.urgency,\n    action: \"Please review and approve\u002Fedit this response\",\n  });\n\n  \u002F\u002F Process the human's decision\n  if (humanDecision.approved) {\n    return new Command({\n      update: { responseText: humanDecision.editedResponse || state.responseText },\n      goto: \"sendReply\",\n    });\n  } else {\n    return new Command({ update: {}, goto: END });\n  }\n};\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-approval-workflow>\n\n---\n\n## Validation Loop\n\nUse `interrupt()` in a loop to validate human input and re-prompt if invalid.\n\n\u003Cex-validation-loop>\n\u003Cpython>\nValidate human input in a loop, re-prompting until valid.\n```python\nfrom langgraph.types import interrupt\n\ndef get_age_node(state):\n    prompt = \"What is your age?\"\n\n    while True:\n        answer = interrupt(prompt)\n\n        # Validate the input\n        if isinstance(answer, int) and answer > 0:\n            break\n        else:\n            # Invalid input — ask again with a more specific prompt\n            prompt = f\"'{answer}' is not a valid age. Please enter a positive number.\"\n\n    return {\"age\": answer}\n```\n\nEach `Command(resume=...)` call provides the next answer. If invalid, the loop re-interrupts with a clearer message.\n```python\nconfig = {\"configurable\": {\"thread_id\": \"form-1\"}}\nfirst = graph.invoke({\"age\": None}, config)\n# __interrupt__: \"What is your age?\"\n\nretry = graph.invoke(Command(resume=\"thirty\"), config)\n# __interrupt__: \"'thirty' is not a valid age...\"\n\nfinal = graph.invoke(Command(resume=30), config)\nprint(final[\"age\"])  # 30\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nValidate human input in a loop, re-prompting until valid.\n```typescript\nimport { interrupt } from \"@langchain\u002Flanggraph\";\n\nconst getAgeNode = (state: typeof State.State) => {\n  let prompt = \"What is your age?\";\n\n  while (true) {\n    const answer = interrupt(prompt);\n\n    \u002F\u002F Validate the input\n    if (typeof answer === \"number\" && answer > 0) {\n      return { age: answer };\n    } else {\n      \u002F\u002F Invalid input — ask again with a more specific prompt\n      prompt = `'${answer}' is not a valid age. Please enter a positive number.`;\n    }\n  }\n};\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-validation-loop>\n\n---\n\n## Multiple Interrupts\n\nWhen parallel branches each call `interrupt()`, resume all of them in a single invocation by mapping each interrupt ID to its resume value.\n\n\u003Cex-multiple-interrupts>\n\u003Cpython>\nResume multiple parallel interrupts by mapping interrupt IDs to values.\n```python\nfrom typing import Annotated, TypedDict\nimport operator\nfrom langgraph.checkpoint.memory import InMemorySaver\nfrom langgraph.graph import START, END, StateGraph\nfrom langgraph.types import Command, interrupt\n\nclass State(TypedDict):\n    vals: Annotated[list[str], operator.add]\n\ndef node_a(state):\n    answer = interrupt(\"question_a\")\n    return {\"vals\": [f\"a:{answer}\"]}\n\ndef node_b(state):\n    answer = interrupt(\"question_b\")\n    return {\"vals\": [f\"b:{answer}\"]}\n\ngraph = (\n    StateGraph(State)\n    .add_node(\"a\", node_a)\n    .add_node(\"b\", node_b)\n    .add_edge(START, \"a\")\n    .add_edge(START, \"b\")\n    .add_edge(\"a\", END)\n    .add_edge(\"b\", END)\n    .compile(checkpointer=InMemorySaver())\n)\n\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\n\n# Both parallel nodes hit interrupt() and pause\nresult = graph.invoke({\"vals\": []}, config)\n# result[\"__interrupt__\"] contains both Interrupt objects with IDs\n\n# Resume all pending interrupts at once using a map of id -> value\nresume_map = {\n    i.id: f\"answer for {i.value}\"\n    for i in result[\"__interrupt__\"]\n}\nresult = graph.invoke(Command(resume=resume_map), config)\n# result[\"vals\"] = [\"a:answer for question_a\", \"b:answer for question_b\"]\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nResume multiple parallel interrupts by mapping interrupt IDs to values.\n```typescript\nimport { Command, END, MemorySaver, START, StateGraph, interrupt, isInterrupted, INTERRUPT, Annotation } from \"@langchain\u002Flanggraph\";\n\nconst State = Annotation.Root({\n  vals: Annotation\u003Cstring[]>({\n    reducer: (left, right) => left.concat(Array.isArray(right) ? right : [right]),\n    default: () => [],\n  }),\n});\n\nfunction nodeA(_state: typeof State.State) {\n  const answer = interrupt(\"question_a\") as string;\n  return { vals: [`a:${answer}`] };\n}\n\nfunction nodeB(_state: typeof State.State) {\n  const answer = interrupt(\"question_b\") as string;\n  return { vals: [`b:${answer}`] };\n}\n\nconst graph = new StateGraph(State)\n  .addNode(\"a\", nodeA)\n  .addNode(\"b\", nodeB)\n  .addEdge(START, \"a\")\n  .addEdge(START, \"b\")\n  .addEdge(\"a\", END)\n  .addEdge(\"b\", END)\n  .compile({ checkpointer: new MemorySaver() });\n\nconst config = { configurable: { thread_id: \"1\" } };\n\nconst interruptedResult = await graph.invoke({ vals: [] }, config);\n\n\u002F\u002F Resume all pending interrupts at once\nconst resumeMap: Record\u003Cstring, string> = {};\nif (isInterrupted(interruptedResult)) {\n  for (const i of interruptedResult[INTERRUPT]) {\n    if (i.id != null) {\n      resumeMap[i.id] = `answer for ${i.value}`;\n    }\n  }\n}\nconst result = await graph.invoke(new Command({ resume: resumeMap }), config);\n\u002F\u002F result.vals = [\"a:answer for question_a\", \"b:answer for question_b\"]\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-multiple-interrupts>\n\nUser-fixable errors use `interrupt()` to pause and collect missing data — that's the pattern covered by this skill. For the full 4-tier error handling strategy (RetryPolicy, Command error loops, etc.), see the **fundamentals** skill.\n\n---\n\n## Side Effects Before Interrupt Must Be Idempotent\n\nWhen the graph resumes, the node restarts from the **beginning** — ALL code before `interrupt()` re-runs. In subgraphs, BOTH the parent node and the subgraph node re-execute.\n\n\u003Cidempotency-rules>\n\n**Do:**\n- Use **upsert** (not insert) operations before `interrupt()`\n- Use **check-before-create** patterns\n- Place side effects **after** `interrupt()` when possible\n- Separate side effects into their own nodes\n\n**Don't:**\n- Create new records before `interrupt()` — duplicates on each resume\n- Append to lists before `interrupt()` — duplicate entries on each resume\n\n\u003C\u002Fidempotency-rules>\n\n\u003Cex-idempotent-patterns>\n\u003Cpython>\nIdempotent operations before interrupt vs non-idempotent (wrong).\n```python\n# GOOD: Upsert is idempotent — safe before interrupt\ndef node_a(state: State):\n    db.upsert_user(user_id=state[\"user_id\"], status=\"pending_approval\")\n    approved = interrupt(\"Approve this change?\")\n    return {\"approved\": approved}\n\n# GOOD: Side effect AFTER interrupt — only runs once\ndef node_a(state: State):\n    approved = interrupt(\"Approve this change?\")\n    if approved:\n        db.create_audit_log(user_id=state[\"user_id\"], action=\"approved\")\n    return {\"approved\": approved}\n\n# BAD: Insert creates duplicates on each resume!\ndef node_a(state: State):\n    audit_id = db.create_audit_log({  # Runs again on resume!\n        \"user_id\": state[\"user_id\"],\n        \"action\": \"pending_approval\",\n    })\n    approved = interrupt(\"Approve this change?\")\n    return {\"approved\": approved}\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nIdempotent operations before interrupt vs non-idempotent (wrong).\n```typescript\n\u002F\u002F GOOD: Upsert is idempotent — safe before interrupt\nconst nodeA = async (state: typeof State.State) => {\n  await db.upsertUser({ userId: state.userId, status: \"pending_approval\" });\n  const approved = interrupt(\"Approve this change?\");\n  return { approved };\n};\n\n\u002F\u002F GOOD: Side effect AFTER interrupt — only runs once\nconst nodeA = async (state: typeof State.State) => {\n  const approved = interrupt(\"Approve this change?\");\n  if (approved) {\n    await db.createAuditLog({ userId: state.userId, action: \"approved\" });\n  }\n  return { approved };\n};\n\n\u002F\u002F BAD: Insert creates duplicates on each resume!\nconst nodeA = async (state: typeof State.State) => {\n  await db.createAuditLog({  \u002F\u002F Runs again on resume!\n    userId: state.userId,\n    action: \"pending_approval\",\n  });\n  const approved = interrupt(\"Approve this change?\");\n  return { approved };\n};\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-idempotent-patterns>\n\n\u003Csubgraph-interrupt-re-execution>\n\n### Subgraph re-execution on resume\n\nWhen a subgraph contains an `interrupt()`, resuming re-executes BOTH the parent node (that invoked the subgraph) AND the subgraph node (that called `interrupt()`):\n\n\u003Cpython>\n```python\ndef node_in_parent_graph(state: State):\n    some_code()  # \u003C-- Re-executes on resume\n    subgraph_result = subgraph.invoke(some_input)\n    # ...\n\ndef node_in_subgraph(state: State):\n    some_other_code()  # \u003C-- Also re-executes on resume\n    result = interrupt(\"What's your name?\")\n    # ...\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\n```typescript\nasync function nodeInParentGraph(state: State) {\n  someCode();  \u002F\u002F \u003C-- Re-executes on resume\n  const subgraphResult = await subgraph.invoke(someInput);\n  \u002F\u002F ...\n}\n\nasync function nodeInSubgraph(state: State) {\n  someOtherCode();  \u002F\u002F \u003C-- Also re-executes on resume\n  const result = interrupt(\"What's your name?\");\n  \u002F\u002F ...\n}\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fsubgraph-interrupt-re-execution>\n\n---\n\n## Command(resume) Warning\n\n`Command(resume=...)` is the **only** Command pattern intended as input to `invoke()`\u002F`stream()`. Do NOT pass `Command(update=...)` as input — it resumes from the latest checkpoint and the graph appears stuck. See the fundamentals skill for the full antipattern explanation.\n\n---\n\n## Fixes\n\n\u003Cfix-checkpointer-required-for-interrupts>\n\u003Cpython>\nCheckpointer required for interrupt functionality.\n```python\n# WRONG\ngraph = builder.compile()\n\n# CORRECT\ngraph = builder.compile(checkpointer=InMemorySaver())\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nCheckpointer required for interrupt functionality.\n```typescript\n\u002F\u002F WRONG\nconst graph = builder.compile();\n\n\u002F\u002F CORRECT\nconst graph = builder.compile({ checkpointer: new MemorySaver() });\n```\n\u003C\u002Ftypescript>\n\u003C\u002Ffix-checkpointer-required-for-interrupts>\n\n\u003Cfix-resume-with-command>\n\u003Cpython>\nUse Command to resume from an interrupt (regular dict restarts graph).\n```python\n# WRONG\ngraph.invoke({\"resume_data\": \"approve\"}, config)\n\n# CORRECT\ngraph.invoke(Command(resume=\"approve\"), config)\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nUse Command to resume from an interrupt (regular object restarts graph).\n```typescript\n\u002F\u002F WRONG\nawait graph.invoke({ resumeData: \"approve\" }, config);\n\n\u002F\u002F CORRECT\nawait graph.invoke(new Command({ resume: \"approve\" }), config);\n```\n\u003C\u002Ftypescript>\n\u003C\u002Ffix-resume-with-command>\n\n\u003Cboundaries>\n### What You Should NOT Do\n\n- Use interrupts without a checkpointer — will fail\n- Resume without the same thread_id — creates a new thread instead of resuming\n- Pass `Command(update=...)` as invoke input — graph appears stuck (use plain dict)\n- Perform non-idempotent side effects before `interrupt()` — creates duplicates on resume\n- Assume code before `interrupt()` only runs once — it re-runs every resume\n\u003C\u002Fboundaries>\n","\u003Coverview>\nLangGraph's human-in-the-loop patterns let you pause graph execution, surface data to users, and resume with their input:\n\n- **`interrupt(value)`** — pauses execution, surfaces a value to the caller\n- **`Command(resume=value)`** — resumes execution, providing the value back to `interrupt()`\n- **Checkpointer** — required to save state while paused\n- **Thread ID** — required to identify which paused execution to resume\n\u003C\u002Foverview>\n\n---\n\n## Requirements\n\nThree things are required for interrupts to work:\n\n1. **Checkpointer** — compile with `checkpointer=InMemorySaver()` (dev) or `PostgresSaver` (prod)\n2. **Thread ID** — pass `{\"configurable\": {\"thread_id\": \"...\"}}` to every `invoke`\u002F`stream` call\n3. **JSON-serializable payload** — the value passed to `interrupt()` must be JSON-serializable\n\n---\n\n## Basic Interrupt + Resume\n\n`interrupt(value)` pauses the graph. The value surfaces in the result under `__interrupt__`. `Command(resume=value)` resumes — the resume value becomes the return value of `interrupt()`.\n\n**Critical**: when the graph resumes, the node restarts from the **beginning** — all code before `interrupt()` re-runs.\n\n\u003Cex-basic-interrupt-resume>\n\u003Cpython>\nPause execution for human review and resume with Command.\n```python\nfrom langgraph.types import interrupt, Command\nfrom langgraph.checkpoint.memory import InMemorySaver\nfrom langgraph.graph import StateGraph, START, END\nfrom typing_extensions import TypedDict\n\nclass State(TypedDict):\n    approved: bool\n\ndef approval_node(state: State):\n    # Pause and ask for approval\n    approved = interrupt(\"Do you approve this action?\")\n    # When resumed, Command(resume=...) returns that value here\n    return {\"approved\": approved}\n\ncheckpointer = InMemorySaver()\ngraph = (\n    StateGraph(State)\n    .add_node(\"approval\", approval_node)\n    .add_edge(START, \"approval\")\n    .add_edge(\"approval\", END)\n    .compile(checkpointer=checkpointer)\n)\n\nconfig = {\"configurable\": {\"thread_id\": \"thread-1\"}}\n\n# Initial run — hits interrupt and pauses\nresult = graph.invoke({\"approved\": False}, config)\nprint(result[\"__interrupt__\"])\n# [Interrupt(value='Do you approve this action?')]\n\n# Resume with the human's response\nresult = graph.invoke(Command(resume=True), config)\nprint(result[\"approved\"])  # True\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nPause execution for human review and resume with Command.\n```typescript\nimport { interrupt, Command, MemorySaver, StateGraph, StateSchema, START, END } from \"@langchain\u002Flanggraph\";\nimport { z } from \"zod\";\n\nconst State = new StateSchema({\n  approved: z.boolean().default(false),\n});\n\nconst approvalNode = async (state: typeof State.State) => {\n  \u002F\u002F Pause and ask for approval\n  const approved = interrupt(\"Do you approve this action?\");\n  \u002F\u002F When resumed, Command({ resume }) returns that value here\n  return { approved };\n};\n\nconst checkpointer = new MemorySaver();\nconst graph = new StateGraph(State)\n  .addNode(\"approval\", approvalNode)\n  .addEdge(START, \"approval\")\n  .addEdge(\"approval\", END)\n  .compile({ checkpointer });\n\nconst config = { configurable: { thread_id: \"thread-1\" } };\n\n\u002F\u002F Initial run — hits interrupt and pauses\nlet result = await graph.invoke({ approved: false }, config);\nconsole.log(result.__interrupt__);\n\u002F\u002F [{ value: 'Do you approve this action?', ... }]\n\n\u002F\u002F Resume with the human's response\nresult = await graph.invoke(new Command({ resume: true }), config);\nconsole.log(result.approved);  \u002F\u002F true\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-basic-interrupt-resume>\n\n---\n\n## Approval Workflow\n\nA common pattern: interrupt to show a draft, then route based on the human's decision.\n\n\u003Cex-approval-workflow>\n\u003Cpython>\nInterrupt for human review, then route to send or end based on the decision.\n```python\nfrom langgraph.types import interrupt, Command\nfrom langgraph.graph import StateGraph, START, END\nfrom typing import Literal\nfrom typing_extensions import TypedDict\n\nclass EmailAgentState(TypedDict):\n    email_content: str\n    draft_response: str\n    classification: dict\n\ndef human_review(state: EmailAgentState) -> Command[Literal[\"send_reply\", \"__end__\"]]:\n    \"\"\"Pause for human review using interrupt and route based on decision.\"\"\"\n    classification = state.get(\"classification\", {})\n\n    # interrupt() must come first — any code before it will re-run on resume\n    human_decision = interrupt({\n        \"email_id\": state.get(\"email_content\", \"\"),\n        \"draft_response\": state.get(\"draft_response\", \"\"),\n        \"urgency\": classification.get(\"urgency\"),\n        \"action\": \"Please review and approve\u002Fedit this response\"\n    })\n\n    # Process the human's decision\n    if human_decision.get(\"approved\"):\n        return Command(\n            update={\"draft_response\": human_decision.get(\"edited_response\", state.get(\"draft_response\", \"\"))},\n            goto=\"send_reply\"\n        )\n    else:\n        # Rejection — human will handle directly\n        return Command(update={}, goto=END)\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nInterrupt for human review, then route to send or end based on the decision.\n```typescript\nimport { interrupt, Command, END, GraphNode } from \"@langchain\u002Flanggraph\";\n\nconst humanReview: GraphNode\u003Ctypeof EmailAgentState> = async (state) => {\n  const classification = state.classification!;\n\n  \u002F\u002F interrupt() must come first — any code before it will re-run on resume\n  const humanDecision = interrupt({\n    emailId: state.emailContent,\n    draftResponse: state.responseText,\n    urgency: classification.urgency,\n    action: \"Please review and approve\u002Fedit this response\",\n  });\n\n  \u002F\u002F Process the human's decision\n  if (humanDecision.approved) {\n    return new Command({\n      update: { responseText: humanDecision.editedResponse || state.responseText },\n      goto: \"sendReply\",\n    });\n  } else {\n    return new Command({ update: {}, goto: END });\n  }\n};\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-approval-workflow>\n\n---\n\n## Validation Loop\n\nUse `interrupt()` in a loop to validate human input and re-prompt if invalid.\n\n\u003Cex-validation-loop>\n\u003Cpython>\nValidate human input in a loop, re-prompting until valid.\n```python\nfrom langgraph.types import interrupt\n\ndef get_age_node(state):\n    prompt = \"What is your age?\"\n\n    while True:\n        answer = interrupt(prompt)\n\n        # Validate the input\n        if isinstance(answer, int) and answer > 0:\n            break\n        else:\n            # Invalid input — ask again with a more specific prompt\n            prompt = f\"'{answer}' is not a valid age. Please enter a positive number.\"\n\n    return {\"age\": answer}\n```\n\nEach `Command(resume=...)` call provides the next answer. If invalid, the loop re-interrupts with a clearer message.\n```python\nconfig = {\"configurable\": {\"thread_id\": \"form-1\"}}\nfirst = graph.invoke({\"age\": None}, config)\n# __interrupt__: \"What is your age?\"\n\nretry = graph.invoke(Command(resume=\"thirty\"), config)\n# __interrupt__: \"'thirty' is not a valid age...\"\n\nfinal = graph.invoke(Command(resume=30), config)\nprint(final[\"age\"])  # 30\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nValidate human input in a loop, re-prompting until valid.\n```typescript\nimport { interrupt } from \"@langchain\u002Flanggraph\";\n\nconst getAgeNode = (state: typeof State.State) => {\n  let prompt = \"What is your age?\";\n\n  while (true) {\n    const answer = interrupt(prompt);\n\n    \u002F\u002F Validate the input\n    if (typeof answer === \"number\" && answer > 0) {\n      return { age: answer };\n    } else {\n      \u002F\u002F Invalid input — ask again with a more specific prompt\n      prompt = `'${answer}' is not a valid age. Please enter a positive number.`;\n    }\n  }\n};\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-validation-loop>\n\n---\n\n## Multiple Interrupts\n\nWhen parallel branches each call `interrupt()`, resume all of them in a single invocation by mapping each interrupt ID to its resume value.\n\n\u003Cex-multiple-interrupts>\n\u003Cpython>\nResume multiple parallel interrupts by mapping interrupt IDs to values.\n```python\nfrom typing import Annotated, TypedDict\nimport operator\nfrom langgraph.checkpoint.memory import InMemorySaver\nfrom langgraph.graph import START, END, StateGraph\nfrom langgraph.types import Command, interrupt\n\nclass State(TypedDict):\n    vals: Annotated[list[str], operator.add]\n\ndef node_a(state):\n    answer = interrupt(\"question_a\")\n    return {\"vals\": [f\"a:{answer}\"]}\n\ndef node_b(state):\n    answer = interrupt(\"question_b\")\n    return {\"vals\": [f\"b:{answer}\"]}\n\ngraph = (\n    StateGraph(State)\n    .add_node(\"a\", node_a)\n    .add_node(\"b\", node_b)\n    .add_edge(START, \"a\")\n    .add_edge(START, \"b\")\n    .add_edge(\"a\", END)\n    .add_edge(\"b\", END)\n    .compile(checkpointer=InMemorySaver())\n)\n\nconfig = {\"configurable\": {\"thread_id\": \"1\"}}\n\n# Both parallel nodes hit interrupt() and pause\nresult = graph.invoke({\"vals\": []}, config)\n# result[\"__interrupt__\"] contains both Interrupt objects with IDs\n\n# Resume all pending interrupts at once using a map of id -> value\nresume_map = {\n    i.id: f\"answer for {i.value}\"\n    for i in result[\"__interrupt__\"]\n}\nresult = graph.invoke(Command(resume=resume_map), config)\n# result[\"vals\"] = [\"a:answer for question_a\", \"b:answer for question_b\"]\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nResume multiple parallel interrupts by mapping interrupt IDs to values.\n```typescript\nimport { Command, END, MemorySaver, START, StateGraph, interrupt, isInterrupted, INTERRUPT, Annotation } from \"@langchain\u002Flanggraph\";\n\nconst State = Annotation.Root({\n  vals: Annotation\u003Cstring[]>({\n    reducer: (left, right) => left.concat(Array.isArray(right) ? right : [right]),\n    default: () => [],\n  }),\n});\n\nfunction nodeA(_state: typeof State.State) {\n  const answer = interrupt(\"question_a\") as string;\n  return { vals: [`a:${answer}`] };\n}\n\nfunction nodeB(_state: typeof State.State) {\n  const answer = interrupt(\"question_b\") as string;\n  return { vals: [`b:${answer}`] };\n}\n\nconst graph = new StateGraph(State)\n  .addNode(\"a\", nodeA)\n  .addNode(\"b\", nodeB)\n  .addEdge(START, \"a\")\n  .addEdge(START, \"b\")\n  .addEdge(\"a\", END)\n  .addEdge(\"b\", END)\n  .compile({ checkpointer: new MemorySaver() });\n\nconst config = { configurable: { thread_id: \"1\" } };\n\nconst interruptedResult = await graph.invoke({ vals: [] }, config);\n\n\u002F\u002F Resume all pending interrupts at once\nconst resumeMap: Record\u003Cstring, string> = {};\nif (isInterrupted(interruptedResult)) {\n  for (const i of interruptedResult[INTERRUPT]) {\n    if (i.id != null) {\n      resumeMap[i.id] = `answer for ${i.value}`;\n    }\n  }\n}\nconst result = await graph.invoke(new Command({ resume: resumeMap }), config);\n\u002F\u002F result.vals = [\"a:answer for question_a\", \"b:answer for question_b\"]\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-multiple-interrupts>\n\nUser-fixable errors use `interrupt()` to pause and collect missing data — that's the pattern covered by this skill. For the full 4-tier error handling strategy (RetryPolicy, Command error loops, etc.), see the **fundamentals** skill.\n\n---\n\n## Side Effects Before Interrupt Must Be Idempotent\n\nWhen the graph resumes, the node restarts from the **beginning** — ALL code before `interrupt()` re-runs. In subgraphs, BOTH the parent node and the subgraph node re-execute.\n\n\u003Cidempotency-rules>\n\n**Do:**\n- Use **upsert** (not insert) operations before `interrupt()`\n- Use **check-before-create** patterns\n- Place side effects **after** `interrupt()` when possible\n- Separate side effects into their own nodes\n\n**Don't:**\n- Create new records before `interrupt()` — duplicates on each resume\n- Append to lists before `interrupt()` — duplicate entries on each resume\n\n\u003C\u002Fidempotency-rules>\n\n\u003Cex-idempotent-patterns>\n\u003Cpython>\nIdempotent operations before interrupt vs non-idempotent (wrong).\n```python\n# GOOD: Upsert is idempotent — safe before interrupt\ndef node_a(state: State):\n    db.upsert_user(user_id=state[\"user_id\"], status=\"pending_approval\")\n    approved = interrupt(\"Approve this change?\")\n    return {\"approved\": approved}\n\n# GOOD: Side effect AFTER interrupt — only runs once\ndef node_a(state: State):\n    approved = interrupt(\"Approve this change?\")\n    if approved:\n        db.create_audit_log(user_id=state[\"user_id\"], action=\"approved\")\n    return {\"approved\": approved}\n\n# BAD: Insert creates duplicates on each resume!\ndef node_a(state: State):\n    audit_id = db.create_audit_log({  # Runs again on resume!\n        \"user_id\": state[\"user_id\"],\n        \"action\": \"pending_approval\",\n    })\n    approved = interrupt(\"Approve this change?\")\n    return {\"approved\": approved}\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nIdempotent operations before interrupt vs non-idempotent (wrong).\n```typescript\n\u002F\u002F GOOD: Upsert is idempotent — safe before interrupt\nconst nodeA = async (state: typeof State.State) => {\n  await db.upsertUser({ userId: state.userId, status: \"pending_approval\" });\n  const approved = interrupt(\"Approve this change?\");\n  return { approved };\n};\n\n\u002F\u002F GOOD: Side effect AFTER interrupt — only runs once\nconst nodeA = async (state: typeof State.State) => {\n  const approved = interrupt(\"Approve this change?\");\n  if (approved) {\n    await db.createAuditLog({ userId: state.userId, action: \"approved\" });\n  }\n  return { approved };\n};\n\n\u002F\u002F BAD: Insert creates duplicates on each resume!\nconst nodeA = async (state: typeof State.State) => {\n  await db.createAuditLog({  \u002F\u002F Runs again on resume!\n    userId: state.userId,\n    action: \"pending_approval\",\n  });\n  const approved = interrupt(\"Approve this change?\");\n  return { approved };\n};\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fex-idempotent-patterns>\n\n\u003Csubgraph-interrupt-re-execution>\n\n### Subgraph re-execution on resume\n\nWhen a subgraph contains an `interrupt()`, resuming re-executes BOTH the parent node (that invoked the subgraph) AND the subgraph node (that called `interrupt()`):\n\n\u003Cpython>\n```python\ndef node_in_parent_graph(state: State):\n    some_code()  # \u003C-- Re-executes on resume\n    subgraph_result = subgraph.invoke(some_input)\n    # ...\n\ndef node_in_subgraph(state: State):\n    some_other_code()  # \u003C-- Also re-executes on resume\n    result = interrupt(\"What's your name?\")\n    # ...\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\n```typescript\nasync function nodeInParentGraph(state: State) {\n  someCode();  \u002F\u002F \u003C-- Re-executes on resume\n  const subgraphResult = await subgraph.invoke(someInput);\n  \u002F\u002F ...\n}\n\nasync function nodeInSubgraph(state: State) {\n  someOtherCode();  \u002F\u002F \u003C-- Also re-executes on resume\n  const result = interrupt(\"What's your name?\");\n  \u002F\u002F ...\n}\n```\n\u003C\u002Ftypescript>\n\u003C\u002Fsubgraph-interrupt-re-execution>\n\n---\n\n## Command(resume) Warning\n\n`Command(resume=...)` is the **only** Command pattern intended as input to `invoke()`\u002F`stream()`. Do NOT pass `Command(update=...)` as input — it resumes from the latest checkpoint and the graph appears stuck. See the fundamentals skill for the full antipattern explanation.\n\n---\n\n## Fixes\n\n\u003Cfix-checkpointer-required-for-interrupts>\n\u003Cpython>\nCheckpointer required for interrupt functionality.\n```python\n# WRONG\ngraph = builder.compile()\n\n# CORRECT\ngraph = builder.compile(checkpointer=InMemorySaver())\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nCheckpointer required for interrupt functionality.\n```typescript\n\u002F\u002F WRONG\nconst graph = builder.compile();\n\n\u002F\u002F CORRECT\nconst graph = builder.compile({ checkpointer: new MemorySaver() });\n```\n\u003C\u002Ftypescript>\n\u003C\u002Ffix-checkpointer-required-for-interrupts>\n\n\u003Cfix-resume-with-command>\n\u003Cpython>\nUse Command to resume from an interrupt (regular dict restarts graph).\n```python\n# WRONG\ngraph.invoke({\"resume_data\": \"approve\"}, config)\n\n# CORRECT\ngraph.invoke(Command(resume=\"approve\"), config)\n```\n\u003C\u002Fpython>\n\u003Ctypescript>\nUse Command to resume from an interrupt (regular object restarts graph).\n```typescript\n\u002F\u002F WRONG\nawait graph.invoke({ resumeData: \"approve\" }, config);\n\n\u002F\u002F CORRECT\nawait graph.invoke(new Command({ resume: \"approve\" }), config);\n```\n\u003C\u002Ftypescript>\n\u003C\u002Ffix-resume-with-command>\n\n\u003Cboundaries>\n### What You Should NOT Do\n\n- Use interrupts without a checkpointer — will fail\n- Resume without the same thread_id — creates a new thread instead of resuming\n- Pass `Command(update=...)` as invoke input — graph appears stuck (use plain dict)\n- Perform non-idempotent side effects before `interrupt()` — creates duplicates on resume\n- Assume code before `interrupt()` only runs once — it re-runs every resume\n\u003C\u002Fboundaries>\n",{"name":5,"description":7},"main","1ca81b0a7de199942400f965edd4d7544077ac5d",".claude\u002Fskills\u002Flanggraph-human-in-the-loop","https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai","https:\u002F\u002Fgithub.com\u002FCoralShades","coralshades.com.au","2026-04-18 00:45:07",{"source":8,"skillName":5,"commands":48,"prompt":58,"zipAvailable":59},[49,52,55],{"tool":50,"command":51},"npx","npx skills add CoralShades\u002Facm-ai --skill langgraph-human-in-the-loop",{"tool":53,"command":54},"bunx","bunx skills add CoralShades\u002Facm-ai --skill langgraph-human-in-the-loop",{"tool":56,"command":57},"pnpm","pnpm dlx skills add CoralShades\u002Facm-ai --skill langgraph-human-in-the-loop","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Flanggraph-human-in-the-loop\u002FSKILL.md",true,[61],{"path":62,"resourceType":62,"resourceTypeDesc":63,"sizeBytes":64,"sourceHtmlUrl":65},"LICENSE","许可证",7048,"https:\u002F\u002Fgithub.com\u002Fboisenoise\u002Fskills-collections\u002Fblob\u002Fmain\u002Fskills\u002Fantigravity-sfxmint\u002FLICENSE",[67,91,104,119,125,135],{"skillNo":68,"name":69,"nameZh":6,"description":70,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":71,"categories":72,"keywords":78,"publishedAt":35},"01M6C63HKXA890HHV47WBAX0TB","uncodixfy","Prevents generic AI\u002FCodex UI patterns when generating frontend code. Use this skill whenever generating HTML, CSS, React, Vue, Svelte, or any frontend UI code to enforce clean, human-designed aesthetics inspired by Linear, Raycast, Stripe, and GitHub instead of typical AI-generated UI.","https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai\u002Fblob\u002Fmain\u002F.qwen\u002Fskills\u002Funcodixfy\u002FSKILL.md",[73],{"categoryNo":74,"categoryName":75,"taxonomyType":76,"taxonomyTypeDesc":77},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",[69,79,80,81,82,83,84,85,86,87,88,89,90],"github","frontend","css","codex","react","code","linear","ui","vue","generated","use","html",{"skillNo":92,"name":93,"nameZh":6,"description":94,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":95,"licenseFileUrl":6,"sourceHtmlUrl":96,"categories":97,"keywords":98,"publishedAt":35},"01M6C63FJ2GEBBGZ5XSMV40KXB","runpodctl","Runpod CLI to manage your GPU workloads.","Apache-2.0","https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai\u002Fblob\u002Fmain\u002F.qwen\u002Fskills\u002Frunpodctl\u002FSKILL.md",[],[93,99,100,101,102,103],"macos","cli","linux","gpu","Bash",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":110,"publishedAt":35},"01M6C63DPAV46TFVQ01SFYPJ7X","langgraph-persistence","INVOKE THIS SKILL when your LangGraph needs to persist state, remember conversations, travel through history, or configure subgraph checkpointer scoping. Covers checkpointers, thread_id, time travel, Store, and subgraph persistence modes.","https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai\u002Fblob\u002Fmain\u002F.qwen\u002Fskills\u002Flanggraph-persistence\u002FSKILL.md",[],[111,24,112,113,114,115,116,117,118],"langgraph persistence","persistence","travel","history","scoping","remember","state","configure",{"skillNo":120,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":124,"publishedAt":35},"01M6C63BNS9CQEBTB8RNSSBFPA","https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai\u002Fblob\u002Fmain\u002F.qwen\u002Fskills\u002Flanggraph-human-in-the-loop\u002FSKILL.md",[123],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[26,25,24,27,28,29,30,31,32,33,34],{"skillNo":126,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":131,"publishedAt":35},"01M6C639GVYZFX3NFPV5FBDR4F","langgraph-fundamentals","INVOKE THIS SKILL when writing ANY LangGraph code. Covers StateGraph, state schemas, nodes, edges, Command, Send, invoke, streaming, and error handling.","https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai\u002Fblob\u002Fmain\u002F.qwen\u002Fskills\u002Flanggraph-fundamentals\u002FSKILL.md",[],[132,24,28,117,133,134,31,84],"langgraph fundamentals","streaming","writing",{"skillNo":136,"name":137,"nameZh":6,"description":138,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":139,"categories":140,"keywords":141,"publishedAt":35},"01M6C6375N34YX2GZ3SD3R7XR2","langchain-rag","INVOKE THIS SKILL when building ANY retrieval-augmented generation (RAG) system. Covers document loaders, RecursiveCharacterTextSplitter, embeddings (OpenAI), and vector stores (Chroma, FAISS, Pinecone).","https:\u002F\u002Fgithub.com\u002FCoralShades\u002Facm-ai\u002Fblob\u002Fmain\u002F.qwen\u002Fskills\u002Flangchain-rag\u002FSKILL.md",[],[142,143,144,145,146,147,148,149,150,151],"rag","langchain rag","langchain","embeddings","vector","openai","chroma","faiss","vector stores","system"]