Integration Guide¶
Four steps: three on the agent side, one on your BFA (the backend that receives the resume). See Architecture & Lifecycle for how these connect end-to-end.
1. Decorate your node¶
import hgateway_sdk as hg
@hg.hitl_node
def review_node(state):
...
There's no setup call before this — raise_interrupt auto-initializes the
gateway client from HGATEWAY_AGENT_API_KEY on first use. You only need
hg.init(transport=...) if you're injecting a custom transport (e.g. a fake
for tests); see Client & Decorators.
@hitl_node binds the current LangGraph state, config, and an occurrence
counter into contextvars, so the SDK can build the RuntimeContext
automatically when you call raise_interrupt inside it. Calling
raise_interrupt outside a @hitl_node-decorated node raises HitlNodeRequired
— this decorator always comes first.
2. Build the interrupt content¶
Every interrupt is one of 4 families (HitlType): Approval, Decision,
Context, Edit. Each family has a couple of concrete content classes
(named {SubType}{Family}Content), and each pairs with a typed response
TypedDict you read by key. One worked example per family:
Approval — BinaryApprovalContent¶
from hgateway_sdk import BinaryApprovalContent, Choice
content = BinaryApprovalContent(
prompt=f"Approve this action? {state['summary']}",
choices=[Choice("approve", "Approve"), Choice("reject", "Reject")],
)
# response: BinaryApprovalResponse -> {"decision": str}
Decision — SingleDecisionContent¶
from hgateway_sdk import SingleDecisionContent, Option
content = SingleDecisionContent(
prompt="Which region should we deploy to?",
options=[Option("us", "US"), Option("eu", "EU")],
)
# response: SingleDecisionResponse -> {"selected": str}
Context — FreetextContextContent¶
from hgateway_sdk import FreetextContextContent, FreetextInput
content = FreetextContextContent(
prompt="What is the customer's preferred resolution?",
input=FreetextInput(label="Customer preference", placeholder="Describe in a few sentences…"),
)
# response: FreetextContextResponse -> {"value": str}
Edit — ContentEditContent¶
from hgateway_sdk import ContentEditContent
content = ContentEditContent(
prompt="Review the generated email and edit if needed before sending.",
draft="Hi Jane,\n\nWe're following up on your recent support request…\n\nBest, Support Team",
)
# response: ContentEditResponse -> {"content": str}
The remaining 6 leaf classes (ModifyApprovalContent, MultiDecisionContent,
RankDecisionContent, FormContextContent, ConfirmContextContent,
ToolArgsEditContent) follow the same pattern — see
Content Schemas for the full table.
3. Raise the interrupt¶
resp = hg.raise_interrupt(
"approve-action",
content,
features=RunFeatures(routing=Routing(primary=Recipients(channels=["#ops"]))),
fallback={"decision": "reject"},
)
return {"decision": resp["decision"]}
hitl_key("approve-action") identifies this interrupt site. It should stay the same across resumes of the same node — it's part of the gateway's dedup identity, together with the LangGraph checkpoint namespace.featuresis optional.RunFeatures(routing=...)tells the gateway which channel/user to notify; addttl=Ttl(seconds=..., on_expiry=...)to get escalation for free. See Features & Routing.- Ownership vs fallback — if the gateway accepts, it owns the interaction
(routing, notification, TTL) and your graph suspends until a human responds.
If the gateway is unreachable or declines, the SDK falls back to a local
interrupt(fallback)instead — your own backend can resolve it, and the agent still works without a hard dependency on the gateway.
4. In your BFA: don't double-handle a gateway-owned interrupt¶
Your BFA (the backend that owns the LangGraph checkpointer — see Architecture & Lifecycle) will see every suspended graph, including ones the gateway has taken over. Check the interrupt value before rendering your own UI or resolving it yourself:
state = graph.get_state(config)
for task in state.tasks:
for i in task.interrupts:
if i.value.get("__hgateway_owned__"):
# The gateway is driving this one — routing, notification, TTL,
# and collecting the response. Don't render anything; just wait
# for the gateway's signed resume callback (see Architecture & Lifecycle).
continue
# Not gateway-owned (declined/unreachable fallback, or a plain local
# interrupt) — handle it yourself as you normally would.
If you skip this check, a fallback-unaware BFA will try to resolve an interrupt the gateway already owns, racing the gateway's own resume.
Common pitfalls¶
HitlNodeRequired—raise_interruptcalled outside an@hitl_nodenode. Decorate the calling node.GatewayBootstrapError—HGATEWAY_AGENT_API_KEYisn't set. Set the env var or provide a.env.TtlForwardNeedsSecondary— you setTtl.on_expiry=OnExpiry.FORWARD_TO_SECONDARYwithout also settingRouting.secondary— the gateway has nowhere to escalate to.
See Client & Decorators and Constants & Exceptions for the full API surface.