A reference implementation of a deterministic, stage-driven LangGraph agent that pauses for an asynchronous human-in-the-loop (HIL) step and resumes automatically when the external result arrives. The reference domain is a private banking assistant (adding beneficiaries, credit-limit-increase requests, AML-style background checks), but the underlying pattern applies to any regulated workflow that combines LLM-powered natural language handling with deterministic business logic and long-running external dependencies.
Most agent templates let the LLM decide the next step. That's flexible but non-auditable and unpredictable — unacceptable in regulated industries. This accelerator demonstrates a different approach:
- Deterministic state machine — the LLM handles language (intent classification, field extraction, free-form responses); a LangGraph state machine handles all control flow. The stage drives routing, the LLM never does.
- Async human-in-the-loop via an endpoint-mediated checkpoint write — the graph pauses at
WAITING_FOR_BACKGROUND_CHECKand exits. An external system (compliance, fraud, credit bureau, anything slow) POSTs the result to the agent's/invocationsendpoint withcustom_inputs.background_check_result; the handler writes it into the LangGraph checkpoint in Lakebase viagraph.aupdate_state()and returns immediately without an LLM call. The next user message transparently resumes the graph with the result in state. - Workflow-aware UI — the chat UI surfaces the current stage, intent, and progress via a sidebar badge and filter, driven end-to-end by a
workflow.state.updatedSSE event.
Together these give you an auditable agent that can safely block on minutes-to-hours-long external dependencies without holding a socket open, while keeping the chat UI responsive.
┌────────────────┐ ┌────────────────────────┐ ┌────────────────────┐
│ Next.js Chat │◄───►│ MLflow AgentServer │◄───►│ LangGraph state │
│ UI (workflow- │ SSE │ (@invoke / @stream) │ │ machine │
│ aware sidebar)│ └───┬───────────▲────────┘ │ (deterministic │
└────────────────┘ │ │ │ routing) │
▲ ▼ │ └─────────┬──────────┘
│ ┌─────────────────────┐ │
│ webhook │ Lakebase Postgres │◄─────────────────────┘
│ │ (LangGraph │ checkpoint read/write
│ │ AsyncCheckpointer) │ (handler calls
│ └─────────────────────┘ graph.aupdate_state
│ │ on bg_result POST)
│ │
│ │ POST /invocations with
│ │ custom_inputs.background_check_result
┌───────┴──────────────────────────┴──────┐
│ External async system │
│ (compliance / fraud / credit bureau / │
│ any human approval) │
└─────────────────────────────────────────┘
Three architectural choices make this work:
| Layer | Choice | Why |
|---|---|---|
| Agent control flow | LangGraph with _route_by_stage conditional edge |
Every transition is named, testable, and auditable. LLM output never changes the graph's next step. |
| Checkpointing | Databricks Lakebase Postgres via AsyncCheckpointSaver |
Serverless managed Postgres; survives restarts; supports concurrent async writes from external systems. |
| HIL resume | External system POSTs the result to /invocations; the handler calls graph.aupdate_state() to write into Lakebase and fires an internal webhook to the chat UI |
No long-lived sockets, no polling; the next user message picks up the injected result from the checkpoint. |
For a full state-machine diagram and stage-by-stage walkthrough see agent_app/agent_server/README.md.
The included demo implements a private-banking assistant with two intents. Both run the same flow: collect the required fields, then submit a mandatory background check (async HIL); wait; on approval render an email and send; on denial terminate the workflow.
ADD_BENEFICIARY— collectcustomer_id,beneficiary_name,beneficiary_account,sort_code.REQUEST_CREDIT_LIMIT_INCREASE— collectcustomer_id,card_id,amount,currency,reason.
An 8-stage state machine (START → CLASSIFY_INTENT → GET_TEMPLATE → ASK_FOR_FIELDS ↔ EXTRACT_FIELDS → LOOKUP_CUSTOMER_EMAIL → CUSTOMER_BACKGROUND_CHECK → WAITING_FOR_BACKGROUND_CHECK → CONFIRM → SEND_EMAIL → DONE) drives the UI's step-progress badge and filter controls.
All tools are stub implementations (agent_app/agent_server/tools_stubs.py) — no real banking backend is required to run the demo end-to-end. Each stub supports a scenario keyword to exercise error and edge branches (e.g. ambiguous_email, send_failure, template_not_found).
This is the adaptation path when the pattern fits but banking doesn't:
| Change | Where |
|---|---|
| Rename intents | _INTENT_LABELS in agent_app/agent_server/langgraph_agent.py |
| Change required fields + labels | _TEMPLATES, _FIELD_LABELS in agent_app/agent_server/tools_stubs.py |
| Swap the HIL step | submit_background_check + customer_background_check_submit_node / customer_background_check_resume_node |
| Change stage numbering / progress labels | _STAGE_STEP, _STEP_MESSAGES |
| Change the external resume contract | background_check_result custom_input in agent.py + POST /api/internal/background-check-received in server/src/routes/internal.ts |
| Rebrand the UI | client/src/components/app-sidebar.tsx, greeting.tsx, suggested-actions.tsx |
The framework — stage-driven routing, LLM-for-language/graph-for-flow split, checkpoint-based async resume, workflow-aware SSE events — stays unchanged.
.
├── agent_app/
│ ├── agent_server/ # MLflow ResponsesAgent + LangGraph state machine
│ ├── e2e-chatbot-app-next/ # Next.js chat UI (workflow-aware sidebar, filters, badges)
│ ├── scripts/ # quickstart, discover-tools, start-app
│ ├── databricks.yml # Databricks Asset Bundle
│ └── app.yaml # Databricks App runtime config
└── config/
└── databricks_resources.json # Workspace-level placeholders (fill in via quickstart)
cd agent_app
# 1. Provision the Lakebase checkpoint store (and register the app + MLflow
# experiment resources) via the Databricks Asset Bundle.
databricks bundle deploy
# 2. Authenticate, create the MLflow experiment, wire up .env.
uv run quickstart --lakebase banking-agent-memory
# 3. Start the agent server + chat UI locally on http://localhost:8000.
uv run start-appFull setup, evaluation, and deployment instructions: agent_app/README.md.
- Databricks workspace on AWS, Azure, or GCP with Unity Catalog enabled.
- Foundation Model API enabled with the
databricks-claude-sonnet-4endpoint reachable (the agent hardcodes this endpoint; swap theChatDatabricks(endpoint=...)line inagent_server/agent.pyto use a different one). - Databricks Lakebase available in the workspace. The
databricks bundle deploystep above provisions the Postgres instance used as the LangGraph checkpoint store. - Local:
uv, Node 20 vianvm, and Databricks CLI ≥ 0.298 (older versions fail bundle deploy witherror downloading Terraform: openpgp: key expired;brew upgrade databricks/tap/databricksfixes it).
First-run note. The first time you send a message to the agent,
AsyncCheckpointSaver.setup()runs the LangGraph checkpoint DDL against Lakebase. That can take 30-60s and will appear to hang if you're testing withcurl --max-time 3. Subsequent requests return in under a second.
| Package | License |
|---|---|
| mlflow-skinny[databricks] | Apache-2.0 |
| databricks-langchain | Apache-2.0 |
| langgraph | MIT |
| langchain-core | MIT |
| fastapi | MIT |
| uvicorn | BSD-3-Clause |
| Package | License |
|---|---|
| react | MIT |
| vite | MIT |
| express | MIT |
| drizzle-orm | Apache-2.0 |
| ai (Vercel AI SDK) | Apache-2.0 |
| @radix-ui/* | MIT |
| tailwindcss | MIT |
| biome | MIT |
| playwright | Apache-2.0 |
Exhaustive lists: agent_app/pyproject.toml, agent_app/e2e-chatbot-app-next/package.json, plus the package.json under each workspace.
None. The reference implementation uses synthetic stub data generated at runtime (see tools_stubs.py). Adopters are responsible for the data they connect when replacing stubs.
See LICENSE.
Report security concerns per SECURITY.md.
This project is provided as-is with no warranty. See the Databricks Industry Solutions contribution guidelines for the support model.