API Reference¶
LangGraphApp¶
LangGraphApp(auth_level=func.AuthLevel.FUNCTION, health_auth_level=func.AuthLevel.ANONYMOUS, health_details_auth_level=None, max_stream_response_bytes=1024 * 1024, max_request_body_bytes=1024 * 1024, max_input_depth=32, max_input_nodes=10000, platform_compat=False, async_runs=False, thread_lock=None, observer=None, route_prefix=_ROUTE_PREFIX, _registrations=dict(), _sb_registrations=dict())
dataclass
¶
Wraps LangGraph compiled graphs into Azure Functions HTTP endpoints.
Usage::
from azure_functions_langgraph import LangGraphApp
app = LangGraphApp()
app.register(graph=compiled_graph, name="my_agent")
func_app = app.function_app
This auto-registers:
POST /api/graphs/{name}/invoke— synchronous invocationPOST /api/graphs/{name}/stream— buffered SSE response (not true streaming)GET /api/health— anonymous liveness probe (returns only{"status": "ok"})GET /api/health/details— registered-graph inventory (protected; gated byhealth_details_auth_level)GET /api/graphs/{name}/threads/{thread_id}/state— thread state (StatefulGraph only)
Note
The graph argument must satisfy the :class:LangGraphLike protocol
(i.e. have .invoke() and .stream() methods). This avoids a
hard import dependency on langgraph at the library level.
Note
All /stream responses are buffered SSE — chunks emitted by the
graph are collected during execution and flushed as SSE events after the
run completes, so clients do not receive partial tokens incrementally.
This is a deliberate consequence of the classic
HttpRequest/HttpResponse routing model this package is built on,
not a platform limitation. Azure Functions Python v2 does support true
HTTP streaming (runtime 4.34.1+) via the
azurefunctions-extensions-http-fastapi ASGI extension, but enabling
it switches the entire function app to the FastAPI/ASGI model, which
cannot be mixed with the classic routes used here — an app-wide
architectural change tracked in issue #378 (see the "Streaming:
buffered SSE and the true-streaming migration" section of DESIGN.md).
Note
The default auth_level is :attr:~azure.functions.AuthLevel.FUNCTION,
so deployed endpoints require a function key by default. Pass
auth_level=func.AuthLevel.ANONYMOUS explicitly for public access
(e.g. local development); doing so emits an unconditional UserWarning.
The health_auth_level parameter controls the auth level of the health
endpoint independently and defaults to :attr:~azure.functions.AuthLevel.ANONYMOUS,
which is the conventional choice for liveness/readiness probes. The
anonymous GET /api/health returns only {"status": "ok"} — it never
enumerates registered graphs. The detailed inventory (graph names,
descriptions, and checkpointer status) lives on a separate
GET /api/health/details endpoint gated by health_details_auth_level,
which defaults to the app-level auth_level (FUNCTION) so the
inventory is protected unless explicitly opened up.
Note
Per-thread locking on the native invoke/stream endpoints is
pluggable via :attr:thread_lock. The default,
:class:~azure_functions_langgraph.locks.inprocess.InProcessThreadLock,
is in-process only — it is not distributed across Function App
instances, worker processes, or hosts. For multi-instance production
deployments, supply a distributed backend such as
:class:~azure_functions_langgraph.locks.azure_blob.AzureBlobLeaseThreadLock,
or (for platform-compat runs) enable platform_compat=True with
:class:~azure_functions_langgraph.stores.azure_table.AzureTableThreadStore,
which provides ETag-based atomic locking. Set the
AZFUNC_LANGGRAPH_LOCK_BACKEND environment variable to
distributed to fail-fast at construction if the default
in-process backend is still wired.
function_app
property
¶
Return an azure.functions.FunctionApp with all routes registered.
register(graph, name, description=None, stream=True, auth_level=None, *, request_model=None, response_model=None, async_mode=False)
¶
Register a compiled LangGraph graph.
Args:
graph: Any object satisfying :class:`~protocols.LangGraphLike`
(typically a ``CompiledStateGraph`` from ``langgraph``).
name: Unique name for this graph (used in URL routes).
description: Optional human-readable description.
stream: Whether to enable the stream endpoint for this graph.
auth_level: Override app-level auth for this graph's endpoints.
When ``None`` (default), the app-level ``auth_level`` is used.
request_model: Optional Pydantic model class for request body
(used by the metadata / bridge API, not for runtime validation).
response_model: Optional Pydantic model class for response body
(used by the metadata / bridge API, not for runtime validation).
async_mode: When True, route this graph's invoke/stream endpoints
through async Azure Functions handlers that await the graph's
ainvoke / astream methods. Defaults to False (the sync
path), so existing CompiledStateGraph deployments — which expose
both sync and async methods — keep their current behavior. A graph
that exposes only async methods is always served async, regardless
of this flag.
Raises:
TypeError: If *graph* does not satisfy the required protocol, or if
``request_model``/``response_model`` is provided but is not a
Pydantic ``BaseModel`` subclass.
ValueError: If *name* is already registered or invalid.
Source code in src/azure_functions_langgraph/app.py
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 | |
Request models¶
InvokeRequest¶
InvokeRequest
¶
Bases: BaseModel
Request body for graph invocation.
StreamRequest¶
StreamRequest
¶
Bases: BaseModel
Request body for graph streaming.
Response models¶
InvokeResponse¶
InvokeResponse
¶
Bases: BaseModel
Response body for graph invocation.
HealthResponse¶
HealthResponse
¶
Bases: BaseModel
Detailed health response (protected /health/details surface).
ErrorResponse¶
ErrorResponse
¶
Bases: BaseModel
Error response body.
GraphInfo¶
GraphInfo
¶
Bases: BaseModel
Information about a registered graph.
Protocol interfaces¶
InvocableGraph¶
InvocableGraph
¶
Bases: Protocol
Protocol for a graph that supports synchronous invocation.
StreamableGraph¶
StreamableGraph
¶
Bases: Protocol
Protocol for a graph that supports synchronous streaming.
LangGraphLike¶
LangGraphLike
¶
Bases: InvocableGraph, StreamableGraph, Protocol
Protocol combining invoke and stream — matches LangGraph's CompiledStateGraph.
Thread locks¶
Pluggable per-thread lock backends for the native invoke / stream
endpoints. See Operations & API Surface
for the operational guide (RBAC, lease renewal, production checklist).
ThreadLock¶
ThreadLock
¶
Bases: Protocol
Contract for pluggable per-thread lock backends.
Implementations coordinate concurrent access to a native invoke/stream
request that targets a specific (graph_name, thread_id) so that
single-writer checkpointers (for example
:class:~azure_functions_langgraph.checkpointers.azure_blob.AzureBlobCheckpointSaver)
never see racing writes for one thread.
The two shipped implementations are
:class:~azure_functions_langgraph.locks.inprocess.InProcessThreadLock
(the default; single-process only) and
:class:~azure_functions_langgraph.locks.azure_blob.AzureBlobLeaseThreadLock
(distributed via Azure Blob lease CAS).
Third-party backends satisfying this protocol (Redis, Cosmos DB, etc.)
can be plugged in via :attr:LangGraphApp.thread_lock.
acquire(graph_name, thread_id, timeout=0.0)
¶
Attempt to acquire an exclusive lock for (graph_name, thread_id).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
graph_name
|
str
|
Registered graph name. |
required |
thread_id
|
str
|
Thread ID drawn from |
required |
timeout
|
float
|
Maximum seconds to wait for the lock. |
0.0
|
Returns:
| Type | Description |
|---|---|
str | None
|
An opaque, non-empty owner token if the lock was acquired, or |
str | None
|
|
str | None
|
distributed backends, on another Function App instance). The token |
str | None
|
is truthy on success and |
str | None
|
branch on truthiness. Pass the returned token back to |
str | None
|
meth: |
str | None
|
been re-acquired by a different execution. |
Source code in src/azure_functions_langgraph/locks/base.py
release(graph_name, thread_id, token)
¶
Release a lock previously acquired with :meth:acquire.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
graph_name
|
str
|
Registered graph name. |
required |
thread_id
|
str
|
Thread ID that was locked. |
required |
token
|
str
|
The owner token returned by the matching :meth: |
required |
Must be safe to call even if the lock is not currently held by the
caller — implementations should log at DEBUG level for any
inconsistency rather than raising, so that the handler finally
block never masks the underlying request failure.
Source code in src/azure_functions_langgraph/locks/base.py
InProcessThreadLock¶
InProcessThreadLock()
¶
:class:threading.Lock-based per-thread lock scoped to a single worker.
This is the default backend when :attr:LangGraphApp.thread_lock is not
supplied. It is not distributed — locks are held in this Python
interpreter only and do not coordinate across:
- Multiple Azure Functions App instances (scale-out)
- Multiple worker processes on the same instance
- Warm swaps / cold starts
Multi-instance production deployments must supply a distributed
backend such as
:class:~azure_functions_langgraph.locks.azure_blob.AzureBlobLeaseThreadLock.
Thread-safety
Safe for concurrent acquire / release calls from multiple
threads. Internal state is guarded by a private
:class:threading.Lock.
Source code in src/azure_functions_langgraph/locks/inprocess.py
acquire(graph_name, thread_id, timeout=0.0)
¶
Acquire the lock for (graph_name, thread_id).
See :meth:ThreadLock.acquire for the general contract. Returns a
fresh opaque owner token on success, or None if the lock is held.
Source code in src/azure_functions_langgraph/locks/inprocess.py
release(graph_name, thread_id, token)
¶
Release the lock for (graph_name, thread_id).
See :meth:ThreadLock.release for the general contract. A token
that does not match the currently-held owner is a no-op (DEBUG log),
so a stale caller cannot release a lock re-acquired by a newer
execution. On a successful match the owner ref is dropped and the
underlying state is garbage-collected when no other request holds it,
so long-lived workers do not grow the internal dict unboundedly.
Source code in src/azure_functions_langgraph/locks/inprocess.py
AzureBlobLeaseThreadLock¶
AzureBlobLeaseThreadLock(*, container_client, lease_duration=_LEASE_DURATION_MAX, blob_prefix='thread-locks/', auto_renew=True)
¶
Distributed per-thread lock backed by Azure Blob leases.
Coordinates (graph_name, thread_id) locking across multiple Azure
Functions instances by holding an exclusive lease on a marker blob per
thread. Any :class:~azure.storage.blob.ContainerClient will do — the
same container as the AzureBlobCheckpointSaver is a natural fit but
a dedicated container is fine too.
.. warning::
By default this class renews Azure Blob leases in the background
(a per-instance daemon thread renews every active lease at
lease_duration / 3 intervals). Pass auto_renew=False to
opt out — construction then emits a :class:UserWarning because
a finite lease_duration can silently expire mid-execution and
let another instance acquire the same (graph_name, thread_id)
lock, allowing concurrent writes to single-writer checkpointers.
Call :meth:close for graceful shutdown of the renewal thread
when the lock instance is no longer needed; Azure Functions
workers do not typically need this because the daemon thread
dies when the interpreter exits.
Example
from azure.storage.blob import ContainerClient from azure_functions_langgraph import LangGraphApp from azure_functions_langgraph.locks import AzureBlobLeaseThreadLock
container = ContainerClient.from_connection_string(conn, "thread-locks") if not container.exists(): ... container.create_container() lock = AzureBlobLeaseThreadLock(container_client=container) app = LangGraphApp(thread_lock=lock)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
container_client
|
_ContainerClientProtocol
|
An |
required |
lease_duration
|
int
|
Lease length in seconds. Must be 15-60 (finite) or
|
_LEASE_DURATION_MAX
|
blob_prefix
|
str
|
Prefix applied to every marker blob so lock blobs are
visually grouped inside the container. Defaults to
|
'thread-locks/'
|
auto_renew
|
bool
|
If |
True
|
Thread-safety
Safe for concurrent acquire / release calls from multiple
threads. Only one thread in this process can hold a given lease at
a time (the Azure API enforces this globally); this class enforces
it locally by returning False from :meth:acquire when a lease
is already tracked for the key.
Source code in src/azure_functions_langgraph/locks/azure_blob.py
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | |
acquire(graph_name, thread_id, timeout=0.0)
¶
Attempt to hold an Azure Blob lease for (graph_name, thread_id).
Semantics match :meth:ThreadLock.acquire:
timeout=0.0— non-blocking. Returns immediately.timeout>0.0— polls the Azure API with jittered backoff until the lease is acquired or the deadline expires.
A key that is already tracked locally — including one whose lease
has been marked lost but not yet released — returns None
immediately without hitting Azure, so the original execution retains
exclusive local ownership until it releases.
Source code in src/azure_functions_langgraph/locks/azure_blob.py
close()
¶
Stop the renewal thread and release every active lease.
Idempotent and safe to call from any thread. After :meth:close,
further :meth:acquire calls still work but will not be
auto-renewed even if auto_renew=True was passed to the
constructor. Entries already marked lost are dropped without a
service-side release (their lease is gone).
Source code in src/azure_functions_langgraph/locks/azure_blob.py
release(graph_name, thread_id, token)
¶
Release the Azure Blob lease for (graph_name, thread_id).
Best-effort — never raises. If token does not match the owner
token of the currently-tracked lease (e.g. the key was dropped and
re-acquired by a newer execution), release is a no-op logged at DEBUG
so the newer owner is preserved. If the tracked lease was already
marked lost (definitive loss or exhausted renewals), the local
entry is simply cleared without calling lease.release() because
the lease is gone. Otherwise the lease is released best-effort;
failures during release are logged at DEBUG and left for lease expiry
(or manual break) to recover.
Source code in src/azure_functions_langgraph/locks/azure_blob.py
OpenAPI integration¶
Bridges a LangGraphApp into
azure-functions-openapi-python.
register_with_openapi¶
Registers all graph and app-level routes with the OpenAPI package and returns the
number of routes registered. Requires the optional dependency
azure-functions-openapi-python.
from azure_functions_langgraph import LangGraphApp
from azure_functions_langgraph.openapi import register_with_openapi
app = LangGraphApp()
# ... app.register(...) your graphs ...
count = register_with_openapi(app) # -> int (routes registered)
register_with_openapi(app)
¶
Register all graph and app-level endpoints with azure-functions-openapi-python.
Reads the metadata exposed by :meth:LangGraphApp.get_app_metadata and
calls :func:azure_functions_openapi.register_openapi_metadata for each
route.
.. deprecated:: 0.6.0
Request/response shape now also flows through the shared
_azure_functions_metadata["endpoint"] namespace, which
azure-functions-openapi reads directly from each handler (see
issue #294; umbrella azure-functions-validation#270). This bridge is
retained through the deprecation cycle to keep supplying documentation
metadata (summary/description/tags) that the endpoint
namespace intentionally does not carry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
app
|
LangGraphApp
|
A :class: |
required |
Returns:
| Type | Description |
|---|---|
int
|
Number of routes registered with the openapi package. |
Raises:
| Type | Description |
|---|---|
ImportError
|
If |
TypeError
|
If a |