Skip to course content
Free LangGraph course

LangGraph for Agentic Workflows

Unit 05.00: Wrapping an action so it cannot run twice

The moment a graph does something outside itself - sends an email, charges a card, creates a ticket - retries stop being free. A retry loop plus an unguarded action is a duplicate-action generator.

An idempotency key derived from the run

The action takes a key. If the key has been seen, the call returns the previous result instead of acting again. The external system, or a table you own, holds the mapping.

The code calls the same send three times with one key.

SENT = {}


def send_email(to, body, idempotency_key):
    """The key makes a repeated call a no-op instead of a second email."""
    if idempotency_key in SENT:
        return {"status": "already sent", "id": SENT[idempotency_key]}
    message_id = f"msg-{len(SENT) + 1}"
    SENT[idempotency_key] = message_id
    return {"status": "sent", "id": message_id}


key = "run-8841:notify-customer"
for attempt in (1, 2, 3):
    print(f"attempt {attempt}: {send_email('[email protected]', 'hello', key)}")

print(f"\nemails actually sent: {len(SENT)}")

# The key has to be derived from the run and the step, not generated inside the
# call -- a fresh uuid per attempt makes every retry look like new work, which
# is exactly the bug this prevents.

Three attempts, one email. The second and third return already sent along with the original id, so the caller gets a usable result rather than an error.

The key's construction is the part that goes wrong. It has to be derived from the run and the step - run-8841:notify-customer - so that every retry of that step produces the same key. A key generated inside the call, or from a timestamp, makes each retry look like new work, which is exactly the bug the mechanism was added to prevent.

The mistake this prevents

The mistake is deduplicating on the arguments instead of on a key. Two genuinely separate refunds for the same amount to the same account are identical in their arguments and must both go through; two retries of one refund are also identical and must not. Only a key tied to the run can tell them apart.

Takeaway

Give every outward action an idempotency key derived from the run id and the step name. Retries then become safe by construction rather than by everyone remembering.