Yesterday you wrapped a flaky call in retry. The retry doubles your tool's call count — once that fails, once that succeeds. That's only safe if the call is idempotent: re-running produces the same outcome.
Foundations introduced this. Patterns asks you to predict it for unfamiliar shapes. Five sketches:
fetch_emails(max_results=5) — read.send_email(to=..., subject=...) — write, no uniqueness key.create_event(summary="daily", start=...) — write, name not unique.update_event(event_id="abc", summary="new") — write, known event_id.append_row(sheet_id=..., row=["a", "b"]) — write, no uniqueness key.Which are idempotent? (Run twice = same world state.)
# Predicted answers
# 1. idempotent (reads don't change state)
# 2. NOT idempotent (two emails)
# 3. NOT idempotent (two events with same summary, different IDs)
# 4. idempotent (same event_id + same value = same end state)
# 5. NOT idempotent (two rows)Updates by ID are always idempotent?
When the target and the new value are both fixed, yes. Update event abc to summary "new" once: summary is "new". Update again: summary is still "new". World state matches.
If the new value depends on the current value (update(event_id, summary=current_summary + " updated")), it's no longer idempotent — running twice appends " updated" twice.
And how do I make a non-idempotent write idempotent?
Two ways. (a) Add a uniqueness key the API recognizes (some APIs accept Idempotency-Key headers). (b) Track sent IDs in a state Sheet (tomorrow's lesson) and check before sending. The second pattern works on any API.
For any operation, ask: "if my script ran twice with no other changes, is the world state at the end the same as if it had run once?"
FETCH, LIST, GET, SEARCH, BATCH_GET — none change server state. Running them twice is wasteful but not destructive.
| Operation | Idempotent? | Reasoning |
|---|---|---|
send_email(to, subject, body) | no | API has no dedup; second send creates a second message |
create_event(summary, start) | no | Two different event_ids, same summary — duplicates |
update_event(event_id, summary=value) | yes | Target fixed; new value fixed; result fixed |
update_event(event_id, summary=current+" v2") | no | New value depends on current — appends repeatedly |
append_row(sheet_id, row) | no | Row appended again on retry — duplicates |
delete_event(event_id) | yes | Already deleted; second delete is no-op (or 404, which you handle) |
set_status(item_id, status="done") | yes | Same target, same value — same end state |
Three levels of effort:
1. The API supports an idempotency key (some Stripe-style APIs):
create_charge(amount=10, idempotency_key="order-123")
# second call with same key returns the first response without re-chargingComposio actions don't generally expose this. Skip to (2) or (3).
2. Check-then-write (read first, write only if missing):
existing = search_for_event(summary="daily")
if not existing:
create_event(summary="daily", ...)Works for any API. Adds a read per write — cheap.
3. State-based dedup (tomorrow's lesson): keep a Sheet of "already-processed" IDs; check before writing.
A retry on a non-idempotent write doubles the side effect. The single biggest 3am-page reason in scheduled automations: "why did everyone get the alert email twice last night?" — answer: retry on a non-idempotent send. Predict, then design.
Yesterday you wrapped a flaky call in retry. The retry doubles your tool's call count — once that fails, once that succeeds. That's only safe if the call is idempotent: re-running produces the same outcome.
Foundations introduced this. Patterns asks you to predict it for unfamiliar shapes. Five sketches:
fetch_emails(max_results=5) — read.send_email(to=..., subject=...) — write, no uniqueness key.create_event(summary="daily", start=...) — write, name not unique.update_event(event_id="abc", summary="new") — write, known event_id.append_row(sheet_id=..., row=["a", "b"]) — write, no uniqueness key.Which are idempotent? (Run twice = same world state.)
# Predicted answers
# 1. idempotent (reads don't change state)
# 2. NOT idempotent (two emails)
# 3. NOT idempotent (two events with same summary, different IDs)
# 4. idempotent (same event_id + same value = same end state)
# 5. NOT idempotent (two rows)Updates by ID are always idempotent?
When the target and the new value are both fixed, yes. Update event abc to summary "new" once: summary is "new". Update again: summary is still "new". World state matches.
If the new value depends on the current value (update(event_id, summary=current_summary + " updated")), it's no longer idempotent — running twice appends " updated" twice.
And how do I make a non-idempotent write idempotent?
Two ways. (a) Add a uniqueness key the API recognizes (some APIs accept Idempotency-Key headers). (b) Track sent IDs in a state Sheet (tomorrow's lesson) and check before sending. The second pattern works on any API.
For any operation, ask: "if my script ran twice with no other changes, is the world state at the end the same as if it had run once?"
FETCH, LIST, GET, SEARCH, BATCH_GET — none change server state. Running them twice is wasteful but not destructive.
| Operation | Idempotent? | Reasoning |
|---|---|---|
send_email(to, subject, body) | no | API has no dedup; second send creates a second message |
create_event(summary, start) | no | Two different event_ids, same summary — duplicates |
update_event(event_id, summary=value) | yes | Target fixed; new value fixed; result fixed |
update_event(event_id, summary=current+" v2") | no | New value depends on current — appends repeatedly |
append_row(sheet_id, row) | no | Row appended again on retry — duplicates |
delete_event(event_id) | yes | Already deleted; second delete is no-op (or 404, which you handle) |
set_status(item_id, status="done") | yes | Same target, same value — same end state |
Three levels of effort:
1. The API supports an idempotency key (some Stripe-style APIs):
create_charge(amount=10, idempotency_key="order-123")
# second call with same key returns the first response without re-chargingComposio actions don't generally expose this. Skip to (2) or (3).
2. Check-then-write (read first, write only if missing):
existing = search_for_event(summary="daily")
if not existing:
create_event(summary="daily", ...)Works for any API. Adds a read per write — cheap.
3. State-based dedup (tomorrow's lesson): keep a Sheet of "already-processed" IDs; check before writing.
A retry on a non-idempotent write doubles the side effect. The single biggest 3am-page reason in scheduled automations: "why did everyone get the alert email twice last night?" — answer: retry on a non-idempotent send. Predict, then design.
Create a free account to get started. Paid plans unlock all tracks.