---
title: Settle an occurrence effect with a receipt
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/occurrences/effect-receipts.mdx
description: Settle a Chronicle occurrence effect with a receipt so a redelivered occurrence does not append its events twice after a crash or lost reply.
lastUpdated: 2026-10-19T00:00:00.000Z
---

Use an effect receipt when a delivered occurrence may append events again after a crash or a lost reply. The receipt and its events become durable together. A repeated attempt returns the first recorded outcome and response instead of appending another batch.

## Before you begin

Use the same namespace-bound event store for receipt lookup and the effect's unit of work. The kernel must advertise `effect-receipts`. Enrollment, lookup and receipt-only recording throw `EffectReceiptsNotSupported` when support is absent or unknown; there is no unreceipted fallback.

An effect key identifies one effect within an occurrence. Choose a stable name, such as `reserve-stock`, and obtain the key through `OccurrenceContext.EffectKey(name)`. The key's identity excludes the delivery attempt and lease epoch. Its `Value` is the idempotency key to pass to external endpoints.

## Commit events and their receipt

1. Look up the effect with `eventStore.Occurrences.GetEffectReceipt(key)`. If it exists, use its recorded outcome and response without repeating the effect.
2. Begin a unit of work and call `UseEffectReceipt(key, response)` before committing. Stage events through the transactional event sequence, not through an immediate append.
3. Call `UseEffectReceipt` again with the same key if the response becomes available later. The call snapshots its JSON; subsequent mutation of the original object does not change the commit. A different key throws `UnitOfWorkEffectReceiptAlreadySet`.
4. Commit the unit, using its owning completion capability when protected reads require one. Inspect `IsSuccess` and `EffectReceipt`.

A successful receipted commit exposes `EffectReceiptResult`. `Recorded` means this attempt recorded it. `AlreadyRecorded` means another attempt won; use `Receipt.Response`, not the response this attempt computed. The initial lookup is an optimization: the atomic commit checks again before validating or appending.

A receipt's outcome is `Appended` with the sequence and first and last positions, `NoEvents`, or `Refused` with reasons. Only the unit of work can record an `Appended` outcome.

## Keep immediate appends inside the effect boundary

An immediate append commits outside the unit of work and its receipt. When a host runs nested commands or application callbacks for one occurrence effect, open `IUnitOfWork.RefuseImmediateAppends()` before executing them. Native event sequences then throw `ImmediateAppendRefused` before resolving event types or providers, serializing, enumerating a batch, publishing append notifications, or contacting the kernel. This covers prepared and routed appends, batches and named-tag variants, including injected event logs, captured sequence references, and sequences from another store.

For example, given an effect `key`, an `eventSourceId`, and an event to stage:

1. Begin the effect's unit with `var unit = eventStore.UnitOfWorkManager.Begin(correlationId);`.
2. Open the owning lifetime with `using var refusal = unit.RefuseImmediateAppends();` before invoking application code.
3. Enroll the receipt with `unit.UseEffectReceipt(key);` and stage the event with `await eventStore.EventLog.Transactional.Append(eventSourceId, @event);`. Nested commands can stage more events in the same unit; they must not complete it.
4. Commit once through `await refusal.Commit();`, then inspect `unit.IsSuccess` and `unit.EffectReceipt`. A call to `eventStore.EventLog.Append(eventSourceId, @event)` anywhere in this flow would instead throw `ImmediateAppendRefused`.
5. Dispose the owning frame in the same scope, including on failure. The `using` declaration handles this; disposal rolls back an unfinished unit and restores the previous append-admission state.

The first frame owns completion **only when the unit has no existing completion owner and is not already enforcing read protection**. If a host has claimed a `DecisionReadCommitOwner` (as the unit-of-work middleware does), or has already called `EnforceReadProtection()`, the frame is refusal-only: its `Commit()` and `Rollback()` throw `ImmediateAppendRefusalRequiresOwner`, and disposing it does not complete or roll back the unit. Continue committing or rolling back through the existing owner; its protected commit path and read dependencies remain intact. For enforced read protection without a claimed capability, the host must claim completion through `ClaimDecisionReadCommitOwnership()` and use `CommitAsOwner()` or `RollbackAsOwner()` on the native unit.

Later calls to `RefuseImmediateAppends()` return borrowed frames: disposing one does not lift refusal, and its `Commit()` and `Rollback()` also throw `ImmediateAppendRefusalRequiresOwner`. When the first refusal frame does own completion, direct unit completion and a second completion-ownership claim throw that exception; use the frame's `Rollback()` to abandon the effect explicitly. `Begin` or `SetCurrent` replacement is refused throughout either kind of refusal lifetime. Refusal remains active after completion, even when the ambient unit has been cleared, until the first frame is disposed. Await an in-flight commit before disposing a completion-owning frame; disposal during commit throws `UnitOfWorkIsCompleting` and does not lift refusal. Disposal restores admission even if a completion callback throws after rollback.

Admission follows the asynchronous execution context across `await` and inherited `Task.Run` work; it does not affect unrelated sibling flows. The trusted owner commit does not temporarily reopen admission, so serializers and append callbacks cannot use it to make another immediate append. Keep the owning frame private to the host rather than passing it to nested application code.

This capability does not turn on read protection, enroll read dependencies, or change revision, redaction, stream-completion, or receipt-recording policies. Ordinary reads remain ordinary reads. If the decision needs protected reads, also use `EnforceReadProtection()` and its read allowances; those rules remain in force, and no read allowance permits an immediate append. Establish the host's completion ownership and read protection before opening a refusal-only frame; refusal never takes that completion ownership away.

Use this boundary when the host must keep all appends in its final atomic commit, especially when that commit carries an effect receipt. Do not use it as an external-system fence or as a substitute for read protection, and leave it off for work that intentionally appends immediately. Third-party unit implementations that do not support the capability throw `UnitOfWorkImmediateAppendRefusalNotSupported`; a unit bound to a sequence without the trusted commit path throws `EventSequenceImmediateAppendRefusalNotSupported` rather than falling back to an immediate append.

## Decisions that append no events

Keep the receipt enrolled even when the decision produces no events. With protected reads, the commit sends the scopes and receipt together through a validate-only append. A scope conflict writes no receipt. This applies to both decision reads and contributed read protection.

Without protected reads, the unit records `NoEvents` through the receipt-only operation. To seal a terminal refusal explicitly, call `eventStore.Occurrences.RecordEffectReceipt(key, new Refused(reasons), response)`. The public receipt-only operation is refused inside enforced read protection; it cannot replace a guarded commit.

A failed append pipeline writes no receipt. Classify the failure before deciding whether to record a permanent refusal. Concurrency conflicts, transport failures and handler exceptions must remain retryable rather than being sealed as successful no-event effects.

## Response and recovery limits

- A response is optional opaque JSON, limited to 64 KiB of serialized UTF-8. Oversized responses throw `EffectReceiptResponseTooLarge` before transport.
- Responses are plaintext and are not crypto-shredded. Store a minimal result without personal data or secrets, not a raw command response.
- One unit of work carries one receipt into one sequence commit. Nested commands sharing that unit belong to the outer effect; they do not receive independent receipts.
- An immediate append bypasses the enrolled receipt. It can duplicate events on redelivery; open an immediate-append refusal frame when executing nested code that must stay inside the effect's commit.
- After a lost acknowledgement, look up the receipt before executing again. The stored outcome wins even if the repeated attempt computed different content.
- A receipt deduplicates Chronicle effects; it does not fence an external system. Exactly-once external effects require that system to honor the effect key.
