LangGraph replay re-fires API calls: key Sume submits per fork

LangGraph time travel re-runs every node after the checkpoint, API calls too. Derive the Sume Idempotency-Key from the body so replays dedupe and forks run.

5 min readSume
All posts

Yes: if a LangGraph node submits a Sume request, replaying or forking from an earlier checkpoint submits it again, because LangGraph re-executes every node after the checkpoint. The fix is on the Sume side of the call: send an Idempotency-Key that is a function of the request body, so a replay with the same body returns the original receipt and a fork with changed state is a different request that runs on purpose.

LangGraph's time travel guide (read 2026-10-11) states that nodes before the checkpoint are not re-executed, while nodes after it re-execute, 'including any LLM calls, API requests, and interrupts'. A paid generation call inside a downstream node is an API request, so it fires again.

What does LangGraph re-run, and what does that do to a Sume call?

The guide describes two operations. Replay returns to a prior checkpoint and runs the nodes after it again. Fork creates a branch from a past checkpoint with modified state and continues from there. Neither reads a cached result for the downstream nodes. The table maps both onto a node that creates a Sume run.

The Sume column comes from the idempotency rules on the Agent Completions page, read the same day.

LangGraph time travel versus a Sume create call (LangGraph docs read 2026-10-11; Sume docs read 2026-10-11)
OperationNodes before checkpointNodes after checkpointSume create with a body-derived key
Replay, same stateNot re-executedRe-executed, API calls fire againSame key, same body: original receipt returned with idempotency_hit true, no second run
Fork, state edited so the instruction changesNot re-executedRe-executed from the edited stateDifferent body gets a different key: a new run starts, as intended
Fork, state edited but the node reuses an old keyNot re-executedRe-executedSame key, different body: 409 idempotency_conflict, nothing runs
Fresh uuid per callNot re-executedRe-executedA new key every time, so every replay is a new run with its own spend

How should the key be built so replays dedupe and forks still run?

The Agent Completions page says that sending a key again returns the original receipt with idempotency_hit: true, and that using the same key with a different payload returns 409 idempotency_conflict. The Jobs and results page gives the same rule for job submits: use a key again only for the same operation and payload.

That gives a simple derivation. Hash the exact request body, prefix it with the business id the graph is working on, and use the result as the key. A replay rebuilds the same body from the same checkpoint state, so it gets the same key. A fork that edits the prompt builds a different body, so it gets a different key and is allowed to start a run. Do not use the checkpoint id or the current time as the key: either one makes each replay look new.

  • Include the spend cap in the hashed body. A cap change is a body change, and generation_spend_cap_usd is required on every Agent Completion.
  • Keep the business id in the key so two threads with the same prompt do not collide.
  • Never reuse one fixed key across edits of the prompt: you will get 409 instead of a run.

A node that is safe to replay

This node hashes the body it is about to send and uses that as the header value. It needs httpx and SUME_API_KEY in the environment. A 200 replay and a 202 fresh run both carry the receipt, so raise_for_status is enough.

import hashlib
import json
import os

import httpx


def submit_node(state: dict) -> dict:
    body = {
        "instruction": state["instruction"],
        "generation_spend_cap_usd": 2,
    }
    digest = hashlib.sha256(
        json.dumps(body, sort_keys=True).encode()
    ).hexdigest()[:24]
    resp = httpx.post(
        "https://api.sume.com/v1/agent/completions",
        headers={
            "Authorization": f"Bearer {os.environ['SUME_API_KEY']}",
            "Idempotency-Key": f"{state['order_id']}-{digest}",
        },
        json=body,
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()["data"]
    return {"run_id": data["id"], "status_url": data["status_url"]}

What did the recent LangGraph releases change here?

The 1.2.13 release notes on the LangGraph releases page (read 2026-10-11) list several checkpoint fixes: forking before replaying an update checkpoint the thread moved past, keeping an update_state on an older checkpoint out of its other branches, and not replaying an abandoned branch into a DeltaChannel fork. Release 1.2.14 lists a single change, the version bump itself.

These are correctness fixes to which state a branch sees. They do not turn a downstream API call into a cached one. The guide's warning still applies after them, so the idempotency key stays your job, not the framework's.

Two limits on what this post can tell you. The Sume docs read for this post do not state how long an idempotency key is remembered, so do not build a process that depends on a key still matching weeks later. And a replay that returns the original receipt only protects the create call: the run itself keeps going once and the receipt's status_url is how the new branch finds it.

Keep the spend gate in front of a fork

A fork with a genuinely new body is a new paid run, and the cap is the only brake on an unattended one. The cap is required on each completion, so a graph that forks often should set it per node and per thread, not from a global default. For the metered rates behind a cap, the Agent Completions page points to the API pricing page.

If your graph pauses for approval before the paid node, see LangGraph interrupt before a paid video call. If a node fails after retries, do not resubmit a paid Sume job covers that case. For the key rules in depth, read what a changed body does to an idempotency key.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume