Skip to content
masterclassBuddyTech Buzz5 fresh blogs · 5:00 AM IST

LangGraph blueprint · Travel

Travel booking with agent handoffs

Flight, hotel and payments agents in one LangGraph StateGraph pass control to each other with Command(goto=..., update=...) over a shared typed state, and a human approves the final itinerary through interrupt() before any money moves. It must never charge the traveller twice, and never charge an amount or itinerary the traveller did not explicitly approve.

Steps
8
Subjective questions
33
Objective questions
33

The problem and the outcome

Problem: Booking a trip spans flight search, hotel search and payment, each with its own tools and failure modes. A single prompt that does all three gets confused, loses holds and cannot safely pause for a human before charging a card.

Outcome: Each specialist agent owns one job and hands off with the trip state intact. The run pauses durably for approval and resumes exactly where it stopped, even after a crash. Payment runs only after an approval that matches the quoted total.

Architecture

Travellerchat or appAgent Serverthread_id per trip01PostgresStoretraveller prefsPostgrescheckpointer08Flight agentLLM + tools02Hotel agentLLM + tools03Payments agentquote builder04Airline / GDS APIsearch + holdHotel inventoryrate + holdApproval nodeinterrupt()05Confirmationevent queueTicketingconfirm holds07Payment gatewayidempotent capture06request / resumeSTARTseat, loyalty prefssearch + hold fareCommand(goto=hotel)search + hold roomdates changedCommand(goto=payments)itinerary + totalapprove this quote?rejected: edit tripapproved, same quotedeclinedcapturedbooking confirmedsave each super-step

How it works, step by step

  1. The traveller's request reaches the graph on LangSmith Agent Server under a thread_id per trip. The StateGraph has a typed state with messages, trip constraints, flight_hold, hotel_hold, quote and approval keys; messages uses a list reducer so agents append rather than overwrite.
  2. The flight agent reads stored preferences from PostgresStore under a namespace such as ("travellers", user_id). It searches and holds a fare through tools, then returns Command(goto="hotel_agent", update={"flight_hold": ...}) to hand off with the hold recorded in shared state.
  3. The hotel agent uses the flight arrival and departure times from state to search and hold a room. If the dates cannot work, it hands back to the flight agent with a Command(goto="flight_agent") and a reason. Otherwise it hands off to payments.
  4. The payments agent re-checks both holds, builds a single quote with a version number, the total and the currency, and writes it to state. No charge happens in this node.
  5. A dedicated approval node calls interrupt() with the exact itinerary and quote. The checkpointer persists the paused run. The traveller later answers through Command(resume={"approved": true, "quote_version": n}), and the node routes on that answer.
  6. If the approval matches the current quote version, the payment tool captures the amount with an idempotency key derived from thread_id and quote_version. A decline or expired hold routes back to the payments or flight agent via Command(goto=...) instead of failing the thread.
  7. Ticketing converts the fare and room holds into confirmed bookings, writes confirmation numbers to state, and publishes a booking-confirmed event for email or SMS. The graph then reaches END.
  8. Every super-step is checkpointed in Postgres, so a crashed worker resumes the same thread. Support staff can inspect get_state_history, and can fork from an earlier checkpoint with update_state to fix a bad hand-off without re-running payment.

Tech stack

  • LangGraph StateGraph with typed state and reducers
  • Command(goto=..., update=...) for agent handoffs
  • interrupt() and Command(resume=...) for human approval
  • PostgresSaver checkpointer keyed by thread_id
  • PostgresStore for cross-trip traveller preferences
  • LangSmith Agent Server for deployment, streaming and tracing
  • Tool-calling LLM for each specialist agent
  • Payment gateway API that supports idempotency keys

Key design decisions

  • Use handoffs instead of a supervisor because the flow is mostly linear (flight, then hotel, then payments) with occasional back-edges. Each agent knows its natural successor, and a supervisor would add an extra LLM hop per turn.
  • Put interrupt() in its own node with no side effects before it. On resume the node re-executes from the top, so any tool call placed before the interrupt would run twice.
  • Bind approval to a quote_version, not just a yes or no. The payment node refuses to charge if the version or total in state differs from the one the traveller approved.
  • Keep holds and the quote in shared state but long-term preferences in the Store. Per-trip data dies with the thread, while seat or loyalty preferences carry across trips.
  • Stream with the updates mode for the UI timeline and messages mode for agent tokens. The traveller sees which agent is active without the backend exposing raw state.

Common mistakes

  • Charging inside the same node as interrupt(), or before it. The resume replays the node and can capture the payment twice unless the call is moved after approval and made idempotent.
  • Fare or room holds expiring while the run waits for approval. Store hold expiry in state and re-price before capture; if the price changed, issue a new quote version and ask again.
  • Hand-off ping-pong between flight and hotel agents when constraints conflict. Cap back-edges with a counter in state and escalate to the traveller after the limit.
  • Returning Command from nodes without declaring the possible destinations in the return type hint. The graph then cannot render or validate the routes, and typos in goto targets surface only at runtime.
  • Using InMemorySaver in production. Paused approvals vanish on restart, so travellers who approve later hit a missing thread.

What to measure

  • Duplicate payment captures per thousand bookings (target is zero)
  • Hold expiry rate between quote and approval
  • Median time from approval to confirmed ticket
  • Average handoffs per completed booking and share of threads hitting the back-edge cap
  • Resume success rate for threads interrupted or crashed mid-run

Most-asked interview questions

Subjective questions 33

1How do you make sure the traveller is never charged twice in this graph?Asked almost every time

Three layers. First, structure: the approval node only calls interrupt() and routes, and the payment call lives in a separate node after it, because interrupt() re-runs its node from the top on resume. Second, idempotency: the payment tool sends an idempotency key built from thread_id and quote_version, so a retry after a crash or a duplicate resume hits the gateway's dedupe. Third, state checks: before calling the gateway, the node reads state and skips capture if a payment_id already exists. The Postgres checkpointer saves after every super-step, so after a worker dies we resume at the last completed node, and the earlier layers cover the window between the gateway call and the checkpoint write.

Sourced from Docs by LangChain (MCP)

2Walk me through the end-to-end architecture of this travel booking graph.Asked almost every time

The traveller's request hits a graph deployed on LangSmith Agent Server, with one thread_id per trip. A single StateGraph holds a typed state: messages, trip constraints, flight_hold, hotel_hold, quote and approval. The flight agent reads preferences from PostgresStore, holds a fare, and hands off to the hotel agent with Command(goto, update). The hotel agent holds a room or hands back to flights if the dates fail. Payments re-checks both holds and writes a versioned quote, but does not charge. A dedicated approval node calls interrupt() with the exact itinerary and quote. On resume, a matching quote_version triggers capture with an idempotency key. Ticketing confirms the bookings and publishes an event. The Postgres checkpointer saves every super-step, so the graph can resume after a crash and support can use time travel.

Sourced from Docs by LangChain (MCP)

3Why is approval a dedicated node instead of an interrupt() call inside the payments agent?Asked almost every time

On resume, LangGraph re-executes the interrupted node from its beginning. It does not continue from the line after interrupt(). If the payments agent re-checked holds, called an LLM or wrote side effects before calling interrupt(), all of that would run again on every resume. That can produce a different quote than the one the traveller saw. A small approval node that only reads the stored quote, calls interrupt(), validates the answer and routes keeps re-execution cheap and deterministic. Capture then lives in a separate node after approval, so the money step runs only once the decision is persisted in state. This separation is also easier to test and to reason about in get_state_history.

Sourced from Docs by LangChain (MCP)

4How do you guarantee the amount charged is exactly what the traveller approved?Asked almost every time

The interrupt payload contains the exact itinerary, total, currency and quote_version taken from state. The resume value must carry the same quote_version. Before capture, the node compares it with the current quote in state, and any mismatch routes back to payments for a fresh quote. The payment tool never takes an amount from LLM output. It reads total and currency from the persisted quote object that the approval matched. The tool also checks that the holds referenced by the quote are still the ones in flight_hold and hotel_hold. With this in place, a changed fare or room, or a hallucinated number, cannot reach the payment provider without a new explicit approval.

Sourced from Docs by LangChain (MCP)

5What happens if the worker crashes after the payment provider captured the money but before the checkpoint was written?Asked almost every time

When the thread resumes from the last checkpoint, the capture node runs again, because its result was never saved. That is why the capture call uses an idempotency key derived from thread_id and quote_version. On the retry, the provider sees the same key and returns the original result instead of charging again. The node then writes the payment reference to state, and that write is checkpointed. I would also record a pending-capture marker before the call and run a reconciliation job that matches provider transactions against thread state. That catches any provider that handles idempotency differently. In this design, a retry costs only latency and never causes a double charge.

Sourced from Docs by LangChain (MCP)

6Payment is captured but the hotel confirmation fails during ticketing. How do you handle it?Asked almost every time

Treat ticketing as a saga step with its own retries. The hotel confirm call gets its own idempotency key, so a retry cannot create two bookings. If the hold has truly been lost, I would not leave the traveller charged with half a trip. The graph routes to a compensation path: either find an equivalent room, which needs a new approval if the price or itinerary changes, or refund or void the hotel portion and tell the traveller. Every step and its outcome is written to state and checkpointed, so support can see exactly where it failed. Refunds also carry idempotency keys. The aim is that money and confirmed bookings always end up consistent, even if that takes a human step.

Sourced from Docs by LangChain (MCP)

7What goes into the quote, and why does it carry a version number?Asked almost every time

The quote holds references to the flight hold and hotel hold, line items, total, currency, the hold expiry times and a monotonically increasing version. The version ties an approval to one exact snapshot. If anything changes, such as a fare rise, a new room or new dates, payments writes a new quote with a higher version. An approval for an older version then no longer matches and cannot trigger capture. The version also feeds the idempotency key with thread_id. Two retries of the same approved quote therefore collapse into one charge, while a legitimately new, re-approved quote gets a distinct key. Without the version, you cannot tell 'same approval retried' apart from 'different amount approved'.

Sourced from Docs by LangChain (MCP)

8Why use Command(goto=...) handoffs here instead of a supervisor agent?Very often asked

The booking flow has a natural order: flight first, because hotel dates depend on arrival, then payments. With handoffs, each agent decides its successor and writes its result in the same Command(update=...), so routing and state change happen atomically in one step. A supervisor would add an extra LLM call on every turn just to pick the next agent, which costs latency and adds another place to make routing mistakes. I would switch to a supervisor if the domain grew to many optional agents, such as visas, insurance or car hire, in any order. At that point central routing is easier to reason about than every agent knowing every other agent.

Sourced from Docs by LangChain (MCP)

9The traveller approves two hours later and the fare has gone up. What happens?Very often asked

The approval answer arrives through Command(resume=...) and carries the quote_version the traveller saw. Before capture, the payment path re-checks hold status and price with the airline and hotel tools. If a hold expired or the total changed, we do not charge. The node writes a new quote with an incremented version and routes back to the approval node, which interrupts again with the new price and a clear diff. Because the checkpointer persisted the thread, nothing about the earlier search is lost. We also track the hold-expiry rate as a metric. If it is high, we shorten the approval window or warn the traveller about the hold deadline in the interrupt payload.

Sourced from Docs by LangChain (MCP)

10How would you design the typed state, and which keys need reducers?Very often asked

I would use a TypedDict or Pydantic model with these keys: messages, trip_constraints, flight_hold, hotel_hold, quote, approval, payment, bookings, plus control fields such as handoff_count and last_handoff_reason. Messages needs a list reducer, such as operator.add or add_messages, so each agent appends rather than overwrites the conversation. The holds, quote and approval should not have reducers. They have overwrite semantics, because only the latest hold or quote is valid, and appending old quotes would create ambiguity about which one was approved. If I wanted an audit trail of past quotes, I would keep a separate quote_history list with an append reducer. The authoritative quote would stay a single overwritten value.

Sourced from Docs by LangChain (MCP)

11How do you stop the hotel and flight agents handing control back and forth forever?Very often asked

I keep a handoff_count, and possibly a list of tried date options, in state. Each hand-back from hotel to flight increments the counter and records a reason, so the flight agent can avoid repeating a combination that already failed. Once a threshold is reached, the graph routes to a node that asks the traveller for flexibility through interrupt(), instead of looping. LangGraph's recursion limit is a backstop. It raises an error after too many super-steps, but it should not be the primary control because it fails the run rather than degrading gracefully. Recording the reasons also helps support inspect the loop in get_state_history.

Sourced from Docs by LangChain (MCP)

12The traveller never answers the approval prompt. What should the system do?Very often asked

The paused run costs nothing while it waits, because it is only a checkpoint in Postgres. The holds, however, expire on the supplier side. I would store hold expiry times in the quote and show them to the traveller. A scheduled job or reminder flow can notify the traveller before expiry. If they resume after expiry, the approval node, or the capture step right after it, re-validates the holds. When the holds are no longer valid, it routes back to the flight or payments agent via Command(goto=...) to re-hold and re-quote, which produces a new version that needs fresh approval. After a long timeout, the thread can be closed politely and the holds released.

Sourced from Docs by LangChain (MCP)

13What belongs in PostgresStore and what belongs in the thread state?Very often asked

Thread state is per trip and short-lived: the messages, constraints, holds, quote, approval and booking results for this one thread_id. The checkpointer saves it at every super-step. PostgresStore holds long-term memory across threads, under namespaces like ("travellers", user_id). Examples are seat and airline preferences, loyalty numbers, preferred hotel chains and dietary or accessibility needs. Optional semantic search can find relevant preferences from free text. I would not put payment card data or one-trip details in the store. The flight agent reads from the store at the start of a trip. Updates to preferences should be deliberate, ideally confirmed by the traveller, so that one odd booking does not overwrite their long-term profile.

Sourced from Docs by LangChain (MCP)

14How do support staff use time travel to fix a bad hand-off, and what are the risks?Very often asked

They call get_state_history for the thread_id and find the checkpoint just before the bad hand-off, for example where the hotel agent used the wrong arrival date. They call update_state on that checkpoint's config to correct the field, which creates a fork. Then they resume from the fork. The risk is that every node after the fork point re-executes, including holds and possibly payment. So forks should be taken after the capture checkpoint when only ticketing needs fixing. If a fork must come earlier, the payment node relies on the idempotency key and the quote_version check. A changed quote forces a fresh approval. I would also restrict update_state to authorised staff and log every fork.

Sourced from Docs by LangChain (MCP)

15How do you secure the resume step so only the traveller can approve a payment?Very often asked

Agent Server sits behind authentication. Each thread is tied to the owning user's identity, and an authorization check confirms the caller owns the thread_id before accepting Command(resume=...). The approval node also validates the payload shape: approved must be a boolean, and quote_version must match the current quote. Free text from the traveller is never treated as approval. For higher-risk amounts, I would add step-up authentication or the payment provider's own strong customer authentication flow at capture. Thread IDs should be unguessable, but they are not a security boundary on their own. Each approval should be logged with the user, the time and the quote version for audit.

Sourced from Docs by LangChain (MCP)

16Which sensitive data must never appear in graph state, checkpoints or traces?Very often asked

Raw card numbers and CVVs must never enter state, messages, checkpoints or LangSmith traces. The traveller enters card details directly with the payment provider, and the graph only holds a tokenised payment method reference. That keeps the agent system out of most of the card-data compliance scope. Passport numbers and dates of birth needed for ticketing should be passed by reference or kept minimal, encrypted at rest, and redacted in traces. Checkpoints are full state snapshots, so whatever you put in state is copied many times. I would set retention policies on old checkpoints and handle deletion requests that cover both the checkpointer and the store.

Sourced from Docs by LangChain (MCP)

17Why derive the idempotency key from thread_id and quote_version instead of generating a random UUID?Very often asked

A random UUID generated inside the capture node would be regenerated every time the node re-runs, for example after a crash, a retry or a replay from a fork. Each retry would look like a new payment to the provider, which defeats idempotency. Deriving the key deterministically from thread_id and quote_version means every re-execution for the same approved quote produces the same key, so the provider deduplicates it. A genuinely new quote version produces a new key, which is correct because it is a new, separately approved amount. If you did use a UUID, you would have to generate it in an earlier node and persist it in state before capture.

Sourced from Docs by LangChain (MCP)

18How does the approval node validate and route on the resume value?Very often asked

After interrupt() returns the resume value, the node checks that it is well-formed: approved is a boolean, and quote_version is present and an integer. If approved is false, it routes to the payments or flight agent with the traveller's reason, or ends politely. If approved is true but quote_version does not match the current quote, it routes back to payments to present the new quote. A malformed payload makes it call interrupt() again with an explanation. Only a matching approval writes approval={version, timestamp} to state and returns Command(goto="capture"). Routing should use Command with type hints listing the allowed destinations, so the graph structure stays explicit.

Sourced from Docs by LangChain (MCP)

19Two browser tabs submit approval for the same trip at the same moment. What happens?Very often asked

There are several layers of protection. Agent Server runs one active run per thread, and a double-texting policy such as reject or enqueue decides what happens to the second submission. If both somehow reach capture, they carry the same thread_id and quote_version, so they produce the same idempotency key, and the provider charges once and returns the same result to both. The second approval also finds approval already recorded for that version and can route straight to the existing result. The traveller sees one charge and one confirmation. I would test this case explicitly, because concurrency bugs around approvals are exactly where double charges come from.

Sourced from Docs by LangChain (MCP)

20The traveller asks to change the hotel after approving but before ticketing completes. How do you handle it?Often asked

If capture has not happened yet, the change invalidates the current approval. The hotel agent holds the new room and payments writes a new quote with a higher version. The old approval no longer matches, so the traveller must approve again. If capture has already happened, it becomes a modification flow: a price difference means an additional charge or a partial refund, each with its own approval and idempotency key, and the original booking may need cancelling under supplier rules. The design principle stays the same. Any change in amount or itinerary creates a new quote version that needs explicit approval before money moves.

Sourced from Docs by LangChain (MCP)

21How would you stream progress to the traveller's app while agents work?Often asked

I would combine stream modes. The 'updates' mode gives per-node state deltas, so the UI can show stages like 'flight held' and 'hotel held'. The 'messages' mode streams LLM tokens with node metadata, so the UI can label which agent is speaking. The 'custom' mode lets tools emit progress events such as 'searching 3 airlines'. When the run hits interrupt(), the client receives the interrupt payload and renders the approval screen with the exact itinerary and quote. I would avoid streaming raw tool outputs that might include supplier data or PII. 'debug' mode is for internal tooling, not end users.

Sourced from Docs by LangChain (MCP)

22How would you estimate checkpoint storage for this system?Often asked

I would avoid guessing and build it from assumptions you can measure. Storage per trip is roughly the number of super-steps per trip multiplied by the average checkpoint size. The steps include agent turns, tool loops, hand-backs, approval, capture and ticketing. Size grows with the message history, since each checkpoint snapshots state. Multiply by trips per day and the retention period. I would measure real sizes in staging, then plan Postgres capacity, indexes on thread_id, and a retention or pruning policy for completed trips. Trimming or summarising long message lists and keeping large supplier payloads out of state both reduce growth.

Sourced from Docs by LangChain (MCP)

23How does this design scale to many concurrent trips?Often asked

Each trip is an independent thread_id, so trips do not share mutable state. Agent Server workers can scale horizontally, because the state lives in the Postgres checkpointer rather than in worker memory. Any worker can pick up a resumed thread. Paused approvals consume only storage, not compute. The practical bottlenecks are Postgres write volume from checkpoints, LLM rate limits and supplier API limits. I would size Postgres for checkpoint writes, use connection pooling, and apply per-supplier rate limiting and backoff in tools. Parallel searches via Send improve latency per trip, but they multiply supplier calls, so concurrency limits matter.

Sourced from Docs by LangChain (MCP)

24What happens if the traveller sends a new message while a run is active on their thread?Often asked

This is the double-texting problem, and Agent Server offers strategies for it. Reject refuses the new run. Enqueue runs it after the current one finishes. Interrupt stops the current run and starts the new one, keeping its progress. Rollback stops the current run and discards its progress. For a booking flow I would generally choose enqueue or reject around the payment section. Interrupting or rolling back mid-capture is risky, although idempotency keys still protect the money. Before approval, interrupting a slow search to apply a changed constraint can be fine. The choice should depend on which node is running.

Sourced from Docs by LangChain (MCP)

25Where would you use Send for map-reduce in this workflow?Often asked

Search is the natural fit. The flight agent's planning step can return Send objects from a conditional edge, one per airline or date option, each with its own per-item state. A search node runs them in parallel, and a reducer on a results key such as operator.add collects the outcomes for the agent to rank. The hotel side can do the same across properties. I would not fan out holds or payments. Holding several fares at once wastes inventory and can incur fees, and payment must stay a single, sequential, idempotent step. Send is for read-mostly exploration, not for side effects.

Sourced from Docs by LangChain (MCP)

26Would you make the flight and hotel agents subgraphs? What are the trade-offs?Often asked

Making each agent a subgraph gives encapsulation. The flight agent's internal tool loop, its retries and its private scratch state stay inside it. Separate teams can develop and test it on its own, and it can be reused elsewhere. The costs are more careful state mapping between parent and child schemas, and handoffs that must navigate to the parent graph rather than to a sibling node directly. Debugging also becomes more nested. For three agents in one product, I would start with flat nodes in one StateGraph and move to subgraphs when an agent's internals grow, or when a hierarchical-teams structure becomes necessary.

Sourced from Docs by LangChain (MCP)

27How would you test this graph before production?Often asked

Unit tests cover each node with mocked tools, using InMemorySaver for checkpoints. Interrupt tests run to the approval node, assert the payload matches the quote, then resume with approved, declined, wrong-version and malformed values. Fault injection kills the run after capture but before the checkpoint, then resumes and asserts exactly one provider charge. Hand-back tests force impossible dates and assert the loop stops and asks the traveller. Replay tests fork from earlier checkpoints and confirm no second charge. Finally, I would run end-to-end tests against supplier and payment sandboxes, plus LLM evaluations on agent decisions using LangSmith datasets.

Sourced from Docs by LangChain (MCP)

28How do you publish the booking-confirmed event reliably?Often asked

Publishing directly after writing state risks two failures: losing the event if the worker crashes in between, or sending duplicates on replay. I would use an outbox pattern. The ticketing step writes the confirmation and an outbox record in the same database transaction, and a relay publishes the outbox records to the message bus. Every event carries a stable booking or thread ID, so the email and SMS consumers deduplicate and the traveller gets one notification. If the graph re-executes ticketing, the deterministic event ID makes the duplicate harmless. Notification failure must never roll back a confirmed booking.

Sourced from Docs by LangChain (MCP)

29How would you debug a trip that ended in a bad state?Often asked

I start with the thread_id. get_state_history shows each checkpoint: which node ran, what it wrote, and where hand-offs went. LangSmith traces show the LLM prompts, tool calls, inputs, outputs and latencies for each agent turn, so I can see why the hotel agent handed back or which tool errored. The payment provider's records, looked up by idempotency key, confirm whether money actually moved. If a fix is needed, support forks from the right checkpoint with update_state. I would also emit structured logs and metrics per node, such as hand-back rate, approval latency and capture failures, to spot patterns across trips.

Sourced from Docs by LangChain (MCP)

30Why does the payments agent re-check both holds before building the quote?Often asked

Holds are time-limited and owned by the suppliers, and some time has passed since the flight agent created the fare hold, especially if the hotel agent handed back. A hold may have expired, or its price or availability may have changed. Re-checking ensures the quote reflects what can actually be ticketed, so the traveller approves a real price rather than a stale one. If a hold has lapsed, payments hands back via Command(goto=...) to re-hold instead of quoting. The check happens again near capture, because the approval may arrive much later. Cheap validation is far better than charging for something that cannot be ticketed.

Sourced from Docs by LangChain (MCP)

31How do you defend against prompt injection from supplier content such as hotel descriptions?Often asked

Treat all tool output as untrusted data, not as instructions. Agents get only the tools they need: the hotel agent cannot call payment, and capture is not an LLM tool at all. Capture is a deterministic node gated by a matching approval. Amounts come from the structured quote, never from free text. Tool results are parsed into typed fields before entering state, and long descriptions can be summarised or stripped before going into prompts. Even if injected text convinced an agent to do something odd, the human approval and the quote_version check stand between the agent and any money movement.

Sourced from Docs by LangChain (MCP)

32How would the design change as you add car hire, insurance and more agents?Sometimes asked

Flat peer-to-peer handoffs get harder to reason about as agents multiply, because each agent must know which agents it can hand to. I would group agents into hierarchical teams. A transport team handles flights and cars, a stay team handles hotels, and a shared payments and approval layer stays a single deterministic section. Each team could become a subgraph. The core invariant does not change: one combined, versioned quote, one explicit approval, and one idempotent capture. Adding products should add line items to the quote, not new payment paths. Typed state also needs schema versioning, so old paused threads still resume.

Sourced from Docs by LangChain (MCP)

33Are there risks in exposing this agent through the Agent Server's MCP endpoint at /mcp?Sometimes asked

MCP makes the agent callable as a tool by other AI clients, which widens the attack surface. Callers may be other agents acting on a user's behalf, so authentication and per-user thread ownership must apply equally at /mcp. The key risk is an external agent trying to answer the approval interrupt itself. Approval must come from the traveller through an authenticated channel. It should not be auto-filled by a calling agent unless the product explicitly allows that and records consent. I would expose search and quote capabilities over MCP, keep approval human-only, and apply rate limits and audit logging.

Sourced from Docs by LangChain (MCP)

Objective questions 33

1A node calls a hotel-hold tool and then calls interrupt() for approval. What happens when the run is resumed with Command(resume=...)?Asked almost every time
  1. AExecution continues from the line right after interrupt()
  2. BThe whole node re-runs from the start, so the hotel-hold tool is called againCorrect answer
  3. CThe graph restarts from START
  4. DThe resume value is ignored unless update_state is called first

Why: On resume, LangGraph re-executes the interrupted node from its beginning, and interrupt() then returns the resume value. Side effects before the interrupt therefore repeat unless they are moved to another node or made idempotent.

Sourced from Docs by LangChain (MCP)

2When a run paused at interrupt() is resumed with Command(resume=...), where does execution restart?Asked almost every time
  1. AAt the beginning of the node that called interrupt()Correct answer
  2. BAt START of the whole graph
  3. CAt the next node after the interrupted one
  4. DAt the line immediately after interrupt()

Why: LangGraph re-executes the interrupted node from its start, and interrupt() returns the resume value on that re-run. Any code before interrupt() therefore runs again.

Sourced from Docs by LangChain (MCP)

3Which idempotency key best prevents double charges in this design?Asked almost every time
  1. AThe current timestamp
  2. BA value derived from thread_id and quote_versionCorrect answer
  3. CThe traveller's email address
  4. DA random UUID generated inside the capture node

Why: A deterministic key is identical on every retry of the same approved quote. A new quote version gets a distinct key.

Sourced from Docs by LangChain (MCP)

4The resume value carries quote_version 3, but state now holds quote version 4. What should happen?Asked almost every time
  1. ACapture the amount from version 3
  2. BFail the thread with an error
  3. CRoute back so the traveller sees and approves the new quoteCorrect answer
  4. DCapture the amount from version 4

Why: An approval is valid only for the exact quote it names. A mismatch needs fresh approval, not a charge.

Sourced from Docs by LangChain (MCP)

5Which checkpointer fits production for this workflow?Asked almost every time
  1. ANo checkpointer, relying on retries
  2. BInMemorySaver
  3. CA local JSON file
  4. DA Postgres checkpointerCorrect answer

Why: Postgres persists checkpoints durably across restarts and workers. InMemorySaver is for development and tests.

Sourced from Docs by LangChain (MCP)

6What does returning Command(goto="payments_agent", update={"hotel_hold": h}) from the hotel agent node do?Very often asked
  1. AIt sends a message to a payments agent in a different thread
  2. BIt applies the state update and routes to payments_agent in one stepCorrect answer
  3. CIt forks a new checkpoint branch for payments
  4. DIt only updates state; routing still needs a separate conditional edge

Why: Command combines a state update with control flow from inside a node. That is how agent handoffs pass both the work result and the next destination together.

Sourced from Docs by LangChain (MCP)

7What does a list reducer such as operator.add on the messages key do?Very often asked
  1. ADeduplicates messages by content
  2. BSorts messages by timestamp
  3. CAppends each node's returned messages to the existing listCorrect answer
  4. DReplaces the list with the latest node's messages

Why: A reducer defines how updates merge into state. operator.add concatenates lists, so agents append to the conversation.

Sourced from Docs by LangChain (MCP)

8A key like quote has no reducer. What happens when a node returns an update for it?Very often asked
  1. ALangGraph raises an error
  2. BThe update is appended to a list
  3. CThe new value overwrites the old oneCorrect answer
  4. DThe update is ignored

Why: Keys without a reducer use overwrite semantics, which suits a single authoritative quote.

Sourced from Docs by LangChain (MCP)

9Checkpoints are saved and looked up primarily by which identifier?Very often asked
  1. Aquote_version
  2. Bnode name
  3. Cthread_idCorrect answer
  4. Duser_id

Why: The checkpointer stores state per thread_id, and this design uses one thread_id per trip.

Sourced from Docs by LangChain (MCP)

10When does LangGraph write a checkpoint during a run?Very often asked
  1. AOnly at END
  2. BOnly when interrupt() is called
  3. COnce per hour
  4. DAt every super-stepCorrect answer

Why: Checkpoints are saved at each super-step, which enables durable execution and time travel.

Sourced from Docs by LangChain (MCP)

11Which API lets support staff list a thread's earlier checkpoints?Very often asked
  1. Astream
  2. BSend
  3. Cget_state_historyCorrect answer
  4. Dupdate_state

Why: get_state_history returns the thread's past checkpoints for inspection, replay or forking.

Sourced from Docs by LangChain (MCP)

12How does support fork from an earlier checkpoint to correct a bad hand-off?Very often asked
  1. ASend a new user message
  2. BDelete the thread and start over
  3. CCall update_state on that checkpoint's config, then resumeCorrect answer
  4. DEdit the Postgres rows directly

Why: update_state against an earlier checkpoint creates a forked checkpoint, and execution can resume from it.

Sourced from Docs by LangChain (MCP)

13After a fork from a checkpoint before payment, why is a double charge still prevented?Very often asked
  1. AThe deterministic idempotency key and the quote_version check deduplicate captureCorrect answer
  2. BForks are read-only
  3. CForked runs never execute tool calls
  4. DLangGraph automatically skips payment nodes

Why: Nodes after the fork re-execute, so safety comes from idempotent capture and version-matched approval.

Sourced from Docs by LangChain (MCP)

14Where does the actual charge happen in this workflow?Very often asked
  1. AIn the capture step after an approval matches the current quote versionCorrect answer
  2. BInside the payments agent when it builds the quote
  3. CInside the flight agent when holding the fare
  4. DInside the approval node before interrupt()

Why: Payments only builds the quote. Money moves only after the approval for that exact version is recorded.

Sourced from Docs by LangChain (MCP)

15The hotel agent finds that the flight dates cannot work. What should it return?Very often asked
  1. ACommand(goto="flight_agent") with a reason in the updateCorrect answer
  2. BCommand(resume=...)
  3. CA Send to payments_agent
  4. DAn exception that ends the thread

Why: The design hands control back to the flight agent with a reason, rather than failing.

Sourced from Docs by LangChain (MCP)

16The payment is declined at capture. What is the intended behaviour?Very often asked
  1. ACharge a different stored card automatically
  2. BRetry the capture forever
  3. CMark the thread failed and stop
  4. DRoute back to the payments or flight agent via Command(goto=...)Correct answer

Why: Declines and expired holds are routed for recovery instead of failing the thread. A different card would need traveller consent.

Sourced from Docs by LangChain (MCP)

17Where should traveller seat and airline preferences that persist across trips live?Very often asked
  1. AThe messages list of each thread
  2. BAn InMemorySaver
  3. CThe quote object
  4. DPostgresStore under a namespace such as ("travellers", user_id)Correct answer

Why: Stores give long-term memory across threads, organised by namespace tuples.

Sourced from Docs by LangChain (MCP)

18What payment data should the graph state hold?Very often asked
  1. AThe card number with the CVV removed
  2. BThe card PIN
  3. CThe full card number and CVV
  4. DA tokenised payment method reference from the providerCorrect answer

Why: Checkpoints copy state many times and traces record it, so only provider tokens belong there.

Sourced from Docs by LangChain (MCP)

19Which component lets a paused approval survive a server restart and resume on the same trip?Often asked
  1. AThe messages streaming mode
  2. BA Send object from a conditional edge
  3. CInMemoryStore
  4. DA checkpointer such as the Postgres saver, keyed by thread_idCorrect answer

Why: Checkpointers persist graph state per thread_id at every super-step, which gives durable execution and resumption. Stores hold long-term cross-thread memory, not the run's execution state.

Sourced from Docs by LangChain (MCP)

20What is a risk of calling a hotel-hold tool before interrupt() in the same node?Often asked
  1. AThe interrupt is skipped
  2. BThe checkpointer refuses to save
  3. CThe hold is automatically confirmed
  4. DThe hold call runs again each time the node re-executes on resumeCorrect answer

Why: Code before interrupt() re-runs on resume, so side effects there can be duplicated.

Sourced from Docs by LangChain (MCP)

21What does returning a list of Send objects from a conditional edge do?Often asked
  1. AFans out to a node with separate per-item state, in parallelCorrect answer
  2. BSends an email to the traveller
  3. CPauses the graph for human input
  4. DWrites to the long-term store

Why: Send enables map-reduce: each Send invokes the target node with its own input.

Sourced from Docs by LangChain (MCP)

22Which part of the workflow should not be fanned out with Send?Often asked
  1. ASearching multiple airlines
  2. BSearching multiple hotels
  3. CFetching reviews
  4. DCapturing paymentCorrect answer

Why: Payment must be a single, sequential, idempotent step. Fan-out suits read-mostly search.

Sourced from Docs by LangChain (MCP)

23How are items organised in a LangGraph Store?Often asked
  1. ABy namespace tuples and keysCorrect answer
  2. BBy node name
  3. CBy super-step number
  4. DBy thread_id only

Why: Stores use namespace tuples, such as ("travellers", user_id), and support optional semantic search.

Sourced from Docs by LangChain (MCP)

24Which stream mode delivers LLM tokens together with node metadata?Often asked
  1. Avalues
  2. Bupdates
  3. CmessagesCorrect answer
  4. Ddebug

Why: The messages mode streams LLM tokens tagged with the node that produced them.

Sourced from Docs by LangChain (MCP)

25Which stream mode emits only the state changes each node made?Often asked
  1. Amessages
  2. Bvalues
  3. CupdatesCorrect answer
  4. Dcustom

Why: updates streams per-node deltas. values streams the full state after each step.

Sourced from Docs by LangChain (MCP)

26What happens if you call interrupt() in a graph compiled without a checkpointer?Often asked
  1. AIt works normally
  2. BThe interrupt is silently ignored
  3. CThe run is saved to the Store instead
  4. DThe run cannot be paused and resumed, because interrupts need a checkpointerCorrect answer

Why: Pausing and resuming rely on persisted state, so interrupt() requires a checkpointer.

Sourced from Docs by LangChain (MCP)

27What is a subgraph in LangGraph?Often asked
  1. AA compiled graph used as a node in another graphCorrect answer
  2. BA read replica of the checkpointer
  3. CA stream mode
  4. DA namespace in the Store

Why: Subgraphs compose graphs as nodes, which encapsulates an agent's internal logic.

Sourced from Docs by LangChain (MCP)

28What is the best way to stop endless hand-backs between the hotel and flight agents?Often asked
  1. ADisable hand-backs entirely
  2. BTrack a hand-off counter in state and ask the traveller after a thresholdCorrect answer
  3. CRely only on supplier timeouts
  4. DRemove the checkpointer

Why: An explicit counter degrades gracefully. The recursion limit is a backstop that fails the run.

Sourced from Docs by LangChain (MCP)

29Which pattern publishes booking-confirmed events without losing or duplicating them?Often asked
  1. APublishing from the approval node
  2. BTransactional outbox with idempotent consumersCorrect answer
  3. CFire-and-forget HTTP call
  4. DStoring events in the messages list

Why: The outbox writes the event in the same transaction as the booking. Stable event IDs let consumers deduplicate.

Sourced from Docs by LangChain (MCP)

30Two tabs submit the same approval at once. What ultimately prevents two charges?Often asked
  1. AThe LLM noticing the duplicate
  2. BThe messages reducer
  3. CBrowser caching
  4. DThe same thread_id and quote_version produce the same idempotency keyCorrect answer

Why: The provider deduplicates captures that share an idempotency key, alongside one-active-run-per-thread handling.

Sourced from Docs by LangChain (MCP)

31Why must the payment amount come from the persisted quote rather than from LLM output?Often asked
  1. AIt is faster
  2. BLLM output cannot contain numbers
  3. CThe checkpointer drops LLM output
  4. DThe approved amount must match exactly, and LLM text can be wrong or manipulatedCorrect answer

Why: Reading total and currency from the approved, versioned quote ensures only the approved amount is charged.

Sourced from Docs by LangChain (MCP)

32At which path does a LangSmith Agent Server deployment expose an MCP endpoint?Sometimes asked
  1. A/mcpCorrect answer
  2. B/tools
  3. C/agent
  4. D/rpc

Why: Agent Server exposes deployed agents over its API and an MCP endpoint at /mcp.

Sourced from Docs by LangChain (MCP)

33Which multi-agent pattern does this workflow mainly use?Sometimes asked
  1. AA single agent with no tools
  2. BA supervisor that routes every step
  3. CBatch map-reduce only
  4. DSwarm-style handoffs via Command(goto=...)Correct answer

Why: Agents pass control directly to each other with Command handoffs over shared typed state.

Sourced from Docs by LangChain (MCP)

Track your progress through every blueprint in your dashboard.

Start learning