---
title: "The Unsent Thunderstorm · example of Effect Discipline · Agentic Atlas"
description: "The Unsent Thunderstorm: one rejected weather bulletin shows when to discard, retry, undo, or stop before acting."
canonical: "https://agentic-atlas.dev/nodes/unsent-thunderstorm"
last-updated: "2026-09-23"
---

1. [Agentic Atlas](https://agentic-atlas.dev/)
2. [Patterns in the Agentic Atlas](https://agentic-atlas.dev/atlas)
3. [Foundations](https://agentic-atlas.dev/nodes/foundations)
4. [Effect Discipline](https://agentic-atlas.dev/nodes/effect-discipline)
5. The Unsent Thunderstorm
1. example of [Effect Discipline](https://agentic-atlas.dev/nodes/effect-discipline)
   # The Unsent Thunderstorm
   **What should you do when a rejected answer has already caused an action?**
   Once a rejected answer has acted, how each action was prepared decides whether retry, undo, or discard is still legal.
   Hook
   one rejected weather bulletin shows when to discard, retry, undo, or stop before acting
   Laws & fences
   - When validation runs last, discarding the rejected answer leaves every earlier action in place.
   - Discard, retry, undo, and stop are different permissions, not stronger or weaker versions of one.
   - An idempotent action, one that converges when repeated, makes retry safe but still leaves its effect.
   - Work kept in a private stage can be discarded because it never reached the live page.
   - When no undo exists for an action, the check must come before the agent may act.
   When to reach
   - Reach for this when a rejected agent run may already have published, sent, or written something.
   - Reach for this when a single retry-on-failure rule is meant to cover every action an agent takes.
   Provenance
   [unsent-thunderstorm/lessons](https://agentic-atlas.dev/nodes/unsent-thunderstorm#lessons) · v1.0.11
   Addresses
   atlas_cards unsent-thunderstorm
2. [Effect Discipline](https://agentic-atlas.dev/nodes/effect-discipline)

The card, in place · its connections drawn edges from atlas_links unsent-thunderstorm

On this plate

[context](https://agentic-atlas.dev/nodes/unsent-thunderstorm#context) [problem-signal](https://agentic-atlas.dev/nodes/unsent-thunderstorm#problem-signal) [choice](https://agentic-atlas.dev/nodes/unsent-thunderstorm#choice) [before](https://agentic-atlas.dev/nodes/unsent-thunderstorm#before) [implementation](https://agentic-atlas.dev/nodes/unsent-thunderstorm#implementation) [result](https://agentic-atlas.dev/nodes/unsent-thunderstorm#result) [verification](https://agentic-atlas.dev/nodes/unsent-thunderstorm#verification) [lessons](https://agentic-atlas.dev/nodes/unsent-thunderstorm#lessons) [relationships](https://agentic-atlas.dev/nodes/unsent-thunderstorm#relationships)

Every section is addressable on its own. Read only the ground you need.

## Context

[Permalink to Context section](https://agentic-atlas.dev/nodes/unsent-thunderstorm#context)

This example explains one problem: **what does “reject and retry” mean after an agent has already changed the world?**

Pocket Weather sells forecasts in jars to people who find the sky needlessly large. An agent writes its release bulletin; a validator rejects the draft because it still contains a `TODO`:

```
# Pocket Weather 1.4

TODO: confirm the indoor thunder limit.
```

If the agent only returned text, the workflow could throw the draft away and try again. But the task also lets the agent update a preview, publish the live page, and send the forecast to subscribers. Those actions survive after the draft is rejected.

The same invalid draft is used in every arm below. Only the way one action is prepared changes, so each recovery difference has one cause.

## Problem signal

[Permalink to Problem signal section](https://agentic-atlas.dev/nodes/unsent-thunderstorm#problem-signal)

The unsafe baseline checks last:

```
agent ──▶ write preview ──▶ publish page ──▶ send forecast ──▶ validate
                                                                      │
                                                                      └── REJECT
```

At `REJECT`, throwing away the answer changes none of the things to its left. The duplicate preview remains, the invalid page is live, and the forecast has already gone out. Discarding the text does not put the thunder back in its jar.

## Choice

[Permalink to Choice section](https://agentic-atlas.dev/nodes/unsent-thunderstorm#choice)

Before granting each action, decide what must happen if validation later says no:

| Arrange this before the action | The rejected run can now… | Class name |
| --- | --- | --- |
| Give the agent no write authority | Be thrown away; nothing happened | Pure / read-only |
| Update one stable preview key | Run again without creating another preview | Idempotent |
| Write only to a private stage | Have its whole stage thrown away | Isolated |
| Save the exact page being replaced | Restore that page, then try again | Reversible-at-a-cost |
| Validate before sending | Be stopped before an action that cannot be undone | Irreversible |

The class name is shorthand for the recovery in the middle column. A generic “retry on failure” rule cannot replace this decision: retry is safe for the keyed preview, but it cannot unsend a forecast.

## Before

[Permalink to Before section](https://agentic-atlas.dev/nodes/unsent-thunderstorm#before)

`demo.py` makes the unsafe baseline concrete. After the invalid draft is rejected, four facts remain in its temporary world: a draft file, two preview entries, an invalid live page, and one sent forecast. The baseline has no single cleanup action because those effects do not share a recovery shape.

## Implementation

[Permalink to Implementation section](https://agentic-atlas.dev/nodes/unsent-thunderstorm#implementation)

`demo.py` first reproduces the raw baseline, then starts each shifted arm in a freshly seeded world with the same candidate. Each arm changes one boundary decision.

### Shift 1 — return a plan, do not act *(pure)*

`describe_only()` returns the intended operations without executing them. The fixture snapshots every file before and after the call and asserts byte-for-byte equality. The [dispatch](https://agentic-atlas.dev/glossary/dispatch) is pure, so rejection licenses **discard**.

### Shift 2 — make repeated previews converge *(idempotent)*

`upsert_preview()` replaces the value at `pocket-weather-1.4`. Calling it twice leaves one preview, which makes **retry** free. The preview remains after rejection — idempotence protects the retry, not the discard.

### Shift 3 — keep the draft off the live page *(isolated)*

`write_isolated()` writes under `staging/pocket-weather-1.4/`. Validation rejects the candidate, the stage is removed, and the live page retains its original bytes. Isolation makes **discard** legal because the candidate was never promoted.

### Shift 4 — save what the publish replaces *(reversible-at-a-cost)*

`publish_with_snapshot()` is still a raw live write, but its footprint is one named file and its exact prior bytes are captured first. Rejection invokes `compensate_release()`, which restores those bytes. The effect is reversible-at-a-cost; **compensation** pays for the discard.

### Shift 5 — check before sending *(irreversible)*

The append-only forecast log has no compensator. The workflow checks the bulletin and confirmation before it calls `send_forecast()`. The invalid payload therefore never reaches the irreversible boundary. The legal move is a **[gate](https://agentic-atlas.dev/glossary/gate)**, not a more elaborate apology after releasing indoor thunder.

## Result

[Permalink to Result section](https://agentic-atlas.dev/nodes/unsent-thunderstorm#result)

One rejected draft now produces five different, checkable outcomes:

```
baseline              ──▶ REJECT   every raw effect remains
pure                  ──▶ DISCARD  nothing happened
idempotent            ──▶ RETRY    one keyed preview remains
isolated              ──▶ DISCARD  private stage removed
reversible-at-a-cost  ──▶ UNDO (compensate)  exact live page restored
irreversible          ──▶ STOP (gate)        forecast never sent
```

These are different permissions, not stronger and weaker versions of one permission. The preview may remain because retrying is safe. The staged draft may be discarded because it never became live. The live page must be restored. The subscriber forecast must never be sent before the check.

## Verification

[Permalink to Verification section](https://agentic-atlas.dev/nodes/unsent-thunderstorm#verification)

Run the example from its directory:

```
python3 demo.py
```

The script uses only the Python standard library and writes only inside temporary directories. It first asserts that the rejected baseline leaves its draft, duplicate previews, live-page mutation, and sent-forecast entry behind. The five independent arms then assert that description is byte-preserving, duplicate preview retries converge to one keyed record, rejected staged work never reaches the live page, compensation restores the exact pre-dispatch bytes, and the gated bulletin sends no forecast.

The append-only log is a local stand-in for an irreversible effect; the fixture does not prove that real message delivery is irreversible in every system. It proves the placement decision: when no compensator is in the [contract](https://agentic-atlas.dev/glossary/contract), the check must precede authority to act.

## Lessons

[Permalink to Lessons section](https://agentic-atlas.dev/nodes/unsent-thunderstorm#lessons)

**Decide how failure recovers before uncorking the weather.** “Reject” handles the answer; effect discipline handles what the answer already did. It changes what the workflow prepares up front: no authority, a stable key, a private stage, an exact undo, or a check before action.

The release furniture is incidental. The same distinctions govern database upserts, generated artifacts, deployment APIs, shared state, and outbound messages. A rejected [return](https://agentic-atlas.dev/glossary/return) tells you *that* recovery is needed; the effect class tells you which recovery is still legal.

The relationships ledger

Evidence-bearing references

## Relationships

Every connection keeps the section where it was found. The map above orients; this ledger carries the evidence.

### Outbound references 0

1. No outbound references.

### Inbound references 1

1. in-slice · occurrence 3
   [Effect Discipline](https://agentic-atlas.dev/nodes/effect-discipline#evidence)
   one rejected weather bulletin shows when to discard, retry, undo, or stop before acting
   Evidence: [Evidence](https://agentic-atlas.dev/nodes/effect-discipline#evidence) · occurrence 3

[↑ back to the top](https://agentic-atlas.dev/nodes/unsent-thunderstorm#content) [← the survey](https://agentic-atlas.dev/atlas)

Node unsent-thunderstorm · corpus 78c0e17 · Catalog revision e0cb75881244b1a82193ca738b82a0d508dd62e822524ae82873ec79d51dbb61