Skip to main content

errand-langgraph

errand-langgraph logo

PyPI version Supported Python versions CI status License

Run LangGraph graphs as errand jobs: background execution with status polling, human-in-the-loop resume, event streaming, smart retries, and an auto-generated FastAPI router — no Celery, no separate broker.

Status: published on PyPI. Submit/status, the FastAPI router, human-in-the-loop resume, SSE streaming, and smart retries are all implemented and tested (100% coverage) — see CHANGELOG.md for what shipped in each version.

Requires Python 3.11+. interrupt() is broken under Python 3.10 in recent langgraph releases (a real, verified upstream bug, not a guess — RuntimeError: Called get_config outside of a runnable context, reproduced in plain langgraph with no errand-langgraph involved). Since human-in-the-loop is this package's flagship feature, 3.10 isn't supported.

Define your graph's state with typing_extensions.TypedDict, not typing.TypedDict. On Python < 3.12, Pydantic v2 can't introspect a typing.TypedDict (PydanticUserError) — mount_graph's request-body validation silently falls back to an unvalidated dict[str, Any] if you get this wrong (with a warning telling you why). from typing_extensions import TypedDict, NotRequired avoids it entirely and works the same on every supported Python version.

Pass a checkpointer. Without one, retries re-run the whole graph from scratch instead of resuming (GraphRunner warns at construction time), and interrupt detection falls back to a less robust signal (still correct, just more exposed to LangGraph's own internal changes across versions — see runner.py's docstring). HITL and efficient retries both assume one.

Install

pip install errand-langgraph[fastapi]

Why

A LangGraph agent run can take anywhere from a few seconds to several minutes. BackgroundTasks in FastAPI gives you no state and no way to poll progress; Celery gives you a broker and a deploy story you probably don't need for a single machine. errand is the middle ground for background jobs in general — this package specializes it for graphs: submit a run, poll its status, resume it after an interrupt(), stream its events, and get retries that resume from the last checkpoint instead of re-running the whole graph (and re-paying for every LLM call along the way).

Quickstart

from fastapi import FastAPI
from langgraph.checkpoint.memory import InMemorySaver

from errand_langgraph import GraphRunner
from errand_langgraph.fastapi import mount_graph

from your_agent import graph  # an uncompiled StateGraph, or a compiled one

runner = GraphRunner(graph, checkpointer=InMemorySaver())
app = FastAPI(lifespan=runner.lifespan)  # starts/drains the worker pool
mount_graph(app, runner, prefix="/agent")

This mounts six endpoints:

Method Path What
POST /agent/runs Submit a run. Body is validated against the graph's own input schema when it's introspectable. Returns {"job_id", "thread_id"}, 202.
GET /agent/runs/{job_id} Current status: {"state", "thread_id", "result", "interrupt", "error"}. state is one of queued, running, succeeded, failed, interrupted. 404 if unknown.
POST /agent/runs/{job_id}/resume Resume an interrupted run. Body is the raw value interrupt() should return (any JSON). Returns a new {"job_id", "thread_id"} on the same thread, 202. 404 if job_id is unknown, 409 if it isn't interrupted.
GET /agent/threads/{thread_id}/state Current graph state for the thread: {"values", "next", "interrupt"}.
GET /agent/threads/{thread_id}/history Checkpoint history, newest first, optional ?limit=.
GET /agent/runs/{job_id}/events text/event-stream of the run's graph state as it executes — one event per completed node, {"seq", "type", "data"}. 404 if job_id is unknown. In-process only, see below.
curl -X POST localhost:8000/agent/runs -d '{"messages": [["user", "hi"]]}'
# {"job_id": "...", "thread_id": "..."}

curl localhost:8000/agent/runs/<job_id>
# {"state": "succeeded", "result": {...}, ...}
# or, if a node called interrupt():
# {"state": "interrupted", "interrupt": [{"id": "...", "value": {...}}], ...}

curl -X POST localhost:8000/agent/runs/<job_id>/resume -d 'true'
# {"job_id": "<new job id>", "thread_id": "<same thread>"}

Without FastAPI, drive the same thing directly:

from errand_langgraph import GraphRunner

runner = GraphRunner(graph, checkpointer=InMemorySaver())
await runner.startup()
handle = await runner.submit({"messages": [("user", "hi")]})
status = await runner.status(handle.job_id)
# after an interrupt:
resumed = await runner.resume(handle.job_id, value=True)
await runner.shutdown()

See examples/basic/ for a runnable submit/poll version, examples/hitl/ for the full interrupt/resume cycle, and examples/streaming/ for SSE — all three actually run end to end (server + client, no LLM calls).

Retries are configured with a RetryPolicy:

from errand_langgraph import GraphRunner, RetryPolicy

runner = GraphRunner(
    graph,
    checkpointer=InMemorySaver(),
    retry=RetryPolicy(max_attempts=5, base_delay=1.0, max_delay=30.0),
)

The default policy (3 attempts, exponential backoff with full jitter) retries timeouts, connection errors, and HTTP 429/5xx — duck-typed off a status_code attribute so it works with any HTTP-based provider SDK without importing it. Everything else (state-validation errors, GraphRecursionError, auth errors, a tool's own exceptions) isn't retried by default. With a checkpointer, a retry resumes from the last completed node instead of re-running the graph — pass your own is_retryable predicate for anything more specific:

RetryPolicy(is_retryable=lambda exc: isinstance(exc, MyProviderRateLimitError))

Pass RetryPolicy(max_attempts=1) to disable retrying entirely.

Don't forget startup. Nothing runs submitted work without a running worker pool — wire lifespan=runner.lifespan into FastAPI(...) (as above) or call await runner.startup() yourself before submitting. submit()/ resume() warn if you forget.

Streaming is in-process only. GET /runs/{id}/events reads from an in-memory pubsub inside the worker that's actually running the graph — it only works when the client's request and that worker share this process. Behind a load balancer with multiple worker processes, the SSE request has to land on the specific process that owns the run; this package doesn't route that for you. Polling GET /runs/{id} always works regardless of process topology — reach for streaming only where the single-process constraint is already true (which is most errand-based deployments, but not all).

Roadmap

The full feature set below is implemented, tested, and published. See CHANGELOG.md for what shipped in each version.

  • Done: GraphRunner.submit/status, mount_graph with polling.
  • Done: human-in-the-loop — interrupt() detection, resume(), POST /runs/{id}/resume, thread state/history endpoints.
  • Done: SSE streaming of graph events (in-process only — documented above, not hidden).
  • Done: smart retries — error classification, backoff with full jitter, resume from the last checkpoint instead of re-running the graph (and re-paying for every LLM call) from scratch.

Not yet built: cancelling an in-flight run, metrics/observability, and a registry for mounting more than one graph under a single FastAPI app (today it's one mount_graph call per graph).

License

MIT

Metadata

Release files for errand-langgraph 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for errand-langgraph 0.1.2
File Size Uploaded
errand_langgraph-0.1.2.tar.gz 30.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for errand-langgraph 0.1.2
File Interpreter ABI Platform
errand_langgraph-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 53.4 kB

Release files / errand_langgraph-0.1.2.tar.gz

Download URL errand_langgraph-0.1.2.tar.gz
Size 30.2 kB
Tags Source
SHA-256 checksum
How to use checksums
c2d51f48326814221b6d720f03907c9be86bf6435041152cd117b211f9ec9101
BLAKE2b-256 checksum
How to use checksums
582d98894115a06345e7d0eba8022ff329f5894eb0849aebb67af8cd11da9549
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.

Transparency log

Release files / errand_langgraph-0.1.2-py3-none-any.whl

Download URL errand_langgraph-0.1.2-py3-none-any.whl
Size 23.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
35d0dc84a92047d36ef033e52a388bc7ac4fff9747123a0c5820ca769d422b96
BLAKE2b-256 checksum
How to use checksums
d549e430962f7c4745fdd4b6b47505ad393ff4e11883e95ec4efe142a4bc2a56
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page