[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6EAPPQT3X50YVY3XYXT3CSK":3,"skill-same:01M6EAPPQT3X50YVY3XYXT3CSK":106},{"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":28,"publishedAt":44,"linkOnly":45,"markdownContent":46,"markdownBody":47,"frontMatter":48,"refName":49,"commitSha":50,"skillDirectory":51,"forksCount":52,"repositoryHtmlUrl":53,"repositoryHomepageUrl":54,"ownerHtmlUrl":55,"ownerBlogUrl":6,"ownerDescription":56,"updatedAt":57,"install":58,"resources":71},"01M6EAPPQT3X50YVY3XYXT3CSK","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.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",66,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FHarmeet10000\u002Fskills\u002Flanggraph-human-in-the-loop\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000038","人力资源","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43],"langgraph","loop","langgraph human in the loop","human","human in the loop","workflows","error handling","validation","human-in-the-loop","command","strategy","approval","resume","patterns","error","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","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002FHarmeet10000\u002Fskills\u002Flanggraph-human-in-the-loop",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill langgraph-human-in-the-loop",{"tool":64,"command":65},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill langgraph-human-in-the-loop",{"tool":67,"command":68},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill langgraph-human-in-the-loop","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002FHarmeet10000\u002Fskills\u002Flanggraph-human-in-the-loop\u002FSKILL.md",true,[72,78,82,86,90,94,98,102],{"path":73,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":76,"sourceHtmlUrl":77},"steps\u002Fstep-01-document-discovery.md","REFERENCE","参考文档",4939,"https:\u002F\u002Fgithub.com\u002FdevelopAgent\u002Fbmad-start\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fbmad-check-implementation-readiness\u002Fsteps\u002Fstep-01-document-discovery.md",{"path":79,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":80,"sourceHtmlUrl":81},"steps\u002Fstep-02-prd-analysis.md",4444,"https:\u002F\u002Fgithub.com\u002FdevelopAgent\u002Fbmad-start\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fbmad-check-implementation-readiness\u002Fsteps\u002Fstep-02-prd-analysis.md",{"path":83,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":84,"sourceHtmlUrl":85},"steps\u002Fstep-03-epic-coverage-validation.md",4058,"https:\u002F\u002Fgithub.com\u002FdevelopAgent\u002Fbmad-start\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fbmad-check-implementation-readiness\u002Fsteps\u002Fstep-03-epic-coverage-validation.md",{"path":87,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":88,"sourceHtmlUrl":89},"steps\u002Fstep-04-ux-alignment.md",3306,"https:\u002F\u002Fgithub.com\u002FdevelopAgent\u002Fbmad-start\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fbmad-check-implementation-readiness\u002Fsteps\u002Fstep-04-ux-alignment.md",{"path":91,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":92,"sourceHtmlUrl":93},"steps\u002Fstep-05-epic-quality-review.md",6716,"https:\u002F\u002Fgithub.com\u002FdevelopAgent\u002Fbmad-start\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fbmad-check-implementation-readiness\u002Fsteps\u002Fstep-05-epic-quality-review.md",{"path":95,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":96,"sourceHtmlUrl":97},"steps\u002Fstep-06-final-assessment.md",3304,"https:\u002F\u002Fgithub.com\u002FdevelopAgent\u002Fbmad-start\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fbmad-check-implementation-readiness\u002Fsteps\u002Fstep-06-final-assessment.md",{"path":99,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":100,"sourceHtmlUrl":101},"templates\u002Freadiness-report-template.md",95,"https:\u002F\u002Fgithub.com\u002FdevelopAgent\u002Fbmad-start\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fbmad-check-implementation-readiness\u002Ftemplates\u002Freadiness-report-template.md",{"path":103,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":104,"sourceHtmlUrl":105},"workflow.md",2798,"https:\u002F\u002Fgithub.com\u002FdevelopAgent\u002Fbmad-start\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fbmad-check-implementation-readiness\u002Fworkflow.md",[107,117,127,136,142,149],{"skillNo":108,"name":109,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":110,"categories":111,"keywords":113,"publishedAt":44},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[112],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[114,115,116],"model","model hierarchy","model hierarchy skill",{"skillNo":118,"name":119,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":120,"categories":121,"keywords":123,"publishedAt":44},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[122],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[124,125,126],"MCP","zoom","zoom mcp",{"skillNo":128,"name":129,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":130,"categories":131,"keywords":132,"publishedAt":44},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[133,134,135,125],"sdk","zoom apps sdk","apps",{"skillNo":137,"name":138,"nameZh":6,"description":6,"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":44},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[138],{"skillNo":143,"name":144,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":148,"publishedAt":44},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[147],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[144],{"skillNo":150,"name":151,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":152,"categories":153,"keywords":154,"publishedAt":44},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[151]]