Skip to content

Architecture & Lifecycle

Every interrupt you register is owned by Ved, the hgateway agent that runs its lifecycle end-to-end — from delivery through resolution and back into your graph.

sequenceDiagram
    participant Agent as Agent (LangGraph + SDK)
    participant Ved as Ved (hgateway)
    participant Human as Human (Slack)
    participant BFA as Your BFA

    Agent->>Ved: register interrupt (HitlSpec)
    Ved-->>Agent: accepted — graph suspends
    Ved->>Human: notify (routing, channel)
    Note over Ved: TTL / escalation / reminders<br/>tracked while waiting
    Human-->>Ved: response
    Ved->>BFA: signed resume callback
    Note over BFA: resumes the graph with<br/>the typed response

What's a BFA?

BFA = backend for agent — your own service that owns the LangGraph checkpointer and process running the agent. It's not a gateway concept with its own SDK class; it's just the name for "your backend" in this flow, because it's the thing that:

  • runs the graph and calls raise_interrupt,
  • owns a callback_url the gateway will POST a resume to, and
  • is responsible for turning that resume payload back into a LangGraph Command(resume=...) that continues the suspended graph.

Ved looks up your BFA's callback_url from the Agent record tied to your HGATEWAY_AGENT_API_KEY — it's configured once per agent, not passed on every call.

The full lifecycle

Raised

Your agent calls raise_interrupt. The SDK POSTs the typed HitlSpec to Ved's register endpoint, authenticated with your agent API key (see Auth: register below). Ved then either accepts (owns the interaction from here) or declines/is unreachable (SDK falls back to a local interrupt(fallback) — see the Integration Guide). If accepted, the SDK suspends the graph with the sentinel {"__hgateway_owned__": True, "hitl_instance_id": "..."} — your BFA checks this to know not to render its own UI (see Integration Guide, step 4).

Delivered

Ved delivers an interactive message (Slack today) to whoever your RunFeatures.routing pointed at. Optionally runs LLM-assisted deliberation before or alongside human resolution.

Interaction

If you set Ttl, Ved tracks expiry in the background while waiting on the human and, on expiry, either applies a default response or forwards to Routing.secondary per OnExpiry — the proactive staleness handling and on-the-fly re-routing described in Why hgateway. Nothing waiting on a human silently vanishes.

Resolved

Ved maps the raw Slack interaction back into the typed response shape your content class expects (BinaryApprovalResponse, SingleDecisionResponse, etc.).

Resumed

Ved POSTs the resume to your BFA's callback_url:

{
  "hitl_instance_id": "…",
  "thread_id": "…",
  "run_id": "…",
  "resume": { "decision": "approve" }
}

authenticated with an HMAC signature your BFA must verify (see Auth: resume below). Your BFA takes the resume value and calls graph.invoke(Command(resume=...), config) (or equivalent) to continue the graph with thread_id.

If your BFA's endpoint doesn't return 2xx, Ved retries with exponential backoff (up to 5 attempts, capped at 30 minutes between tries). After the final attempt fails, the interrupt is marked failed rather than retried forever — check your gateway dashboard if a resume never arrives.

Post-Resolution

Independently of the resume dispatch above, this is where Ved's reasoning capture stack runs — recording the responder's reasoning alongside their resolution for future HITLs to draw on. This is newer surface area (see Why hgateway); check reference/responses.md for what's exposed today before depending on a specific field.

Auth: register (SDK → gateway)

The SDK sends your HGATEWAY_AGENT_API_KEY as a standard bearer token:

Authorization: Bearer <HGATEWAY_AGENT_API_KEY>

The gateway hashes the incoming key and looks it up against your agent's stored key hash — never a plaintext comparison. An invalid or inactive key is rejected before the interrupt is ever registered.

Auth: resume (gateway → BFA)

The gateway signs every resume callback the same way Slack signs its webhooks, so a BFA that already verifies Slack signatures can reuse that logic. Two headers are sent alongside the JSON body:

X-Ved-Signature: v0=<hex hmac-sha256>
X-Ved-Request-Timestamp: <unix timestamp>

The signature is computed as HMAC-SHA256(secret, "v0:{timestamp}:{raw_body}"), hex-encoded, prefixed v0=. To verify on your BFA:

  1. Reject the request if X-Ved-Request-Timestamp is more than 5 minutes from your clock (replay protection).
  2. Recompute HMAC-SHA256(your_shared_secret, f"v0:{timestamp}:{raw_body}") over the exact raw request body bytes (not a re-serialized copy).
  3. Constant-time-compare your result to X-Ved-Signature.

The shared secret is the same one configured for your agent's Slack signature verification — one secret, reused for both.

Common pitfalls

  • Resolving your own copy of a gateway-owned interrupt — always check the __hgateway_owned__ sentinel before your BFA does anything with a suspended graph (see Raised above).
  • Verifying the signature against a re-serialized body — HMAC over json.dumps(parsed_body) instead of the raw bytes will mismatch if key order or whitespace differs. Verify against the raw request body.
  • No response within 5 attempts — if your BFA endpoint is down across all retries, the resume is marked failed, not retried indefinitely. Make sure your callback_url is reliably reachable.

Where the dashboard fits

A separate ops dashboard (BFF + SPA) gives visibility into pending/resolved interrupts across agents. It's not part of the integration path described in this site — nothing here requires it.

Next: Getting Started to install the SDK, then the Integration Guide for the step-by-step.