Skip to content
RasaGet a free licence
Guides for AI teams

Guide · AI product engineer

Add a Rasa chat interface to a LangGraph workflow

Let users ask for a bank-statement summary through Rasa while LangGraph runs the calculation and saves unfinished work.

by Rod Rivera

About 5 minutes

Key takeaways (3)
  • Retain your LangGraph workflow and its saved progress.
  • Use Rasa to ask for the missing month and explain the statement summary.
  • Check that saved work belongs to the customer before resuming it.

A customer asks for a bank-statement summary but leaves out the month. Use Rasa to ask for that detail and resume the existing LangGraph workflow. LangGraph keeps the calculation and its saved progress.

The saved work must still belong to the customer making the request. Our test pauses a workflow with demo A’s statement rows, then requests the same run for demo B. The worker refuses it. Removing only the owner check returns A’s rows labelled as B’s:

Controlled workerRequested ownerSaved ownerReturned result
Owner check presentDemo BDemo Acontext_mismatch; no report
Owner check removedDemo BDemo ADemo A’s £32 total, labelled Demo B

Both controls run the same graph and SQLite checkpoint saver. Only the application’s owner comparison changes. Keep that comparison when adding a conversation to a saved workflow.

Here we give a fictional Jacaranda Bank statement workflow a Rasa conversation. LangGraph keeps its row lookup, calculation, pause and checkpoint. The Mantle skill tells the assistant to ask for the missing month and explain the result. You can run the graph on its own, then add the conversation without rewriting the graph.

Keep the work where it already lives

Suppose your team has a workflow that totals travel spending. Its first node loads the selected demo account’s rows. The next pauses for a month. After the answer, it filters those rows and totals the amounts in pence.

The graph source contains no Rasa import. Its calculation is ordinary Python:

def total_travel(state):
    rows = [r for r in state["rows"] if r["month"] == state["month"] and r["category"] == "travel"]
    return {"status": "complete", "report": {
        "month": state["month"], "currency": "GBP", "pence": sum(r["pence"] for r in rows),
        "row_ids": [r["id"] for r in rows],
    }}

The example has synthetic rows for two demo accounts. For demo A in October 2026, two travel rows total 3200 pence. A grocery row and September’s travel row are excluded. Rasa does not recalculate that total or maintain a second copy of the graph’s rows.

User supplies month Rasa tool month only Application current run + demo owner Worker compare saved owner LangGraph checkpoint original rows + paused node owner agrees Mismatch return no report owner differs Graph filters and totals Rasa explains result resume same run
FigureOne saved workflow, with a conversation around it

Only the month is a model-facing resume argument. The application chooses the run and demo account. That prevents the model from choosing a checkpoint; it does not authenticate a real customer.

A checkpoint key is not an account decision

LangGraph’s interrupt documentation says, “Reusing it resumes the same checkpoint”. Our owner-mismatch control adds an application ownership decision to that documented lookup behaviour. The guide explains how interrupt() pauses a graph and Command(resume=...) continues it with the same thread ID. The interrupted node starts again when resumed. Keep side effects out of the code before that pause unless they are safe to repeat. Our row lookup is a separate earlier node.

The worker creates the graph with a SQLite saver, reads the saved state and compares the account before it resumes:

previous = graph.get_state(config)
if previous.values and previous.values.get("demo_owner") != owner:
    return {"error": "context_mismatch"}

The controlled mutation removes that comparison from the actual handler. It then resumes demo A’s checkpoint while requesting demo B. The original rows remain in the graph, but the handler labels its response with B. The native test catches this mismatch by checking the row IDs and amount, not only status: complete.

After creating the worker environment as shown below, run this from the companion repository root:

tutorials/frameworks-with-rasa/langgraph/.venv/bin/python -B -m unittest discover \
  -s tutorials/frameworks-with-rasa/langgraph/tests -p test_checkpoint.py -v

The native guard-removal control printed this literal line:

owner check removed | requested=demo-b | checkpoint=demo-a | returned=S-102,S-103/3200 pence

The application also chooses a new run once a completed search needs another month. Repeating the same completed month returns the same report. Reusing that completed run for a different month is refused.

Let Rasa collect the missing input

The separate Mantle project has three tools: select the public demo code, start the lookup and answer its missing month. The application keeps the current run in a Session. The resume tool accepts only the month:

The user answers a question; the application chooses the run
@tool(description="Resume the current paused LangGraph lookup with the user's month in YYYY-MM format. Run identity stays application-owned.")
async def answer_statement_month(month: str, context: ToolContext = None) -> ToolResult:
    return ToolResult(llm_response=await session().resume(month))
  1. The tool description asks for a resume, so answering the month should continue the paused lookup rather than start it again. The skill repeats this rule.
  2. The user supplies a month. There is no run ID or demo account argument.
  3. session() gets the current conversation’s application state. The worker request adds its saved run and demo account there, outside the model’s arguments.

The skill instructions ask the user for their public demo code, start the graph once, then wait for the month. They tell the model to ask when the year is unclear. Answering the question calls the resume tool; it must not start another graph.

The user-facing sequence is a design, not a recorded conversation:

Application resultConversation’s next step
demo_selectedAsk about the statement task
needs_monthAsk which month and wait
invalid_monthClarify the month and year; keep the pause
completeExplain the month, total and included rows
lookup_unavailableExplain that the lookup did not complete; offer an explicit retry or stop

Rasa adds the dialogue and explanation around the existing workflow. The graph remains independently runnable and responsible for its own result. Neither service moves money or changes an account in this example.

Run the handoff without a model

Get the pinned companion. The replay keeps the selected worker virtualenv interpreter: resolving its symlink to the base interpreter would lose the worker’s installed packages. It resolves the state directory separately to a real absolute path. Preserve that distinction when launching the two worker processes.

The revision was qualified with Rasa Pro 3.21.0.dev5, LangGraph 1.2.12 and langgraph-checkpoint-sqlite 3.1.1. Keep the separate lockfiles.

git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git fetch origin e7db3283483f8836f28e540349f359f51c71df6f
git switch --detach e7db3283483f8836f28e540349f359f51c71df6f
python3 scripts/workspace.py start tutorials/frameworks-with-rasa/langgraph --owner statement-demo
mkdir -m 700 statement-demo-state
python3 tutorials/frameworks-with-rasa/demo_statement.py \
  --worker-python tutorials/frameworks-with-rasa/langgraph/.venv/bin/python \
  --state-dir statement-demo-state --month 2026-10

Choose a new local state directory. If one already exists, inspect it instead of replacing it. The replay starts one worker process to reach the pause, then another to resume it. The recorded replay prints these two complete lines:

{"question": "Which month, for example 2026-10?", "status": "needs_month"}
{"next_step": "Explain the synthetic travel total and the rows included; this does not move money.", "report": {"currency": "GBP", "month": "2026-10", "pence": 3200, "row_ids": ["S-102", "S-103"]}, "status": "complete"}

These are direct replay records, not model replies. The replay needs no provider key or Rasa licence.

Add the conversation runtime

Create only the selected Mantle environment:

python3 scripts/workspace.py start tutorials/frameworks-with-rasa/langgraph-mantle --owner statement-conversation
export STATEMENT_WORKER_PYTHON="$PWD/tutorials/frameworks-with-rasa/langgraph/.venv/bin/python"
export STATEMENT_STATE_DIR="$(python3 -c 'from pathlib import Path; print(Path("statement-demo-state").resolve())')"
cd tutorials/frameworks-with-rasa/langgraph-mantle
make validate
make train
make run

Before training, set your Rasa licence and OPENAI_API_KEY as described in the companion setup. Keep credentials out of source control. The worker receives neither licence nor model or speech keys. This recipe supplies a localhost text channel; it does not add an audio pipeline.

Use the Rasa quickstart for the local conversation setup. Ask about travel spending with public demo code 111111, then supply October 2026 when asked. The native Mantle binding controls verify the worker’s pause and the month-only Mantle tool. When exercising the conversation, record the question, the answer and the resumed tool result; check that the same saved run completes.

Check the boundary and keep the useful parts

The qualification receipt records 24 distinct controls: 11 graph tests, nine application handoff tests and four Mantle decorated-tool tests. Project validation, licensed training and a cold archive load also passed.

Check the decorated tool and application handoff independently from the companion repository root, using the two environments created above:

STATEMENT_TEST_WORKER_PYTHON="$PWD/tutorials/frameworks-with-rasa/langgraph/.venv/bin/python" \
  tutorials/frameworks-with-rasa/langgraph-mantle/.venv/bin/python -B -m unittest discover \
  -s tutorials/frameworks-with-rasa/langgraph/tests -p 'test_*binding.py' -v
STATEMENT_TEST_WORKER_PYTHON="$PWD/tutorials/frameworks-with-rasa/langgraph/.venv/bin/python" \
  tutorials/frameworks-with-rasa/langgraph-mantle/.venv/bin/python -B -m unittest discover \
  -s tutorials/frameworks-with-rasa/langgraph/tests -p test_handoff.py -v

The qualification records these literal summaries for the native graph, decorated-tool and application handoff suites respectively:

Ran 11 tests in 3.065s
OK
Ran 4 tests in 10.865s
OK
Ran 9 tests in 1.497s
OK

These are local test-suite times. Measure dialogue performance separately.

Run the commands in the companion README to check the real worker across processes and through a native Mantle tool. The application handoff controls refuse a missing run, a wrong owner, an invalid month, an overlapping resume and a late result from a replaced run. They also mutate malformed worker results. Assess production authentication, model accuracy and acoustic behaviour through your real channel.

For an existing application, first preserve its graph and regression tests. Replace these fixture rows with your authorised data access. Give the service owner responsibility for account identity and the saved run mapping; give the conversation designer the missing-input and failure exchanges to review. The domain reviewer should refuse release if a user can choose another customer’s workflow. Check this through your real channel before adding users.

After stopping the example, retire the two managed environments with the owners used above. Keep the lockfiles and any checkpoint data or evidence you still need. Do not delete the database as part of environment cleanup.

Why does changing the month after completion fail?

The completed run describes its original month. The worker returns completed_month_changed if a later resume tries to reinterpret it. Start a new lookup for a new question, then answer its month. The native test test_repeat_resume_returns_same_result_and_correction_needs_new_run checks both behaviours; it does not overwrite the earlier result.

How do I check a failed handoff without more model calls?

Stop the conversation server, then run demo_statement.py with the same selected worker interpreter and a new local state directory. If this direct replay fails, inspect the worker path and state-directory permissions before trying another conversation. The worker must use its own virtualenv Python; resolving that symlink to base Python loses its packages. A replay pass narrows the problem to the conversation path. Check the model’s actual questions in a separately recorded conversation. Keep the failed evidence and run the native binding controls before a separately recorded conversation check.