External effects and bounded idempotency
Deduplicating an incoming event does not prove that an external write happened
once. A service may apply a write before the handler loses its response.
Touba distinguishes event_id, run_id, a per-attempt call_id, and the stable
effect_id of the logical business write.
Declare the external service contract
A touba-manifest-v1 binding declares effect_contract. Every binding in that
manifest must specify either kind: none or kind: external_write with an
explicit retry mode. Missing contracts and retired formats are refused.
The generator leaves a non-executable null until the developer reviews the
actual handler behavior.
{
"kind": "external_write",
"retry_mode": "idempotency_key"
}
This is the effect_contract field, not a complete manifest. Existing handler,
grant, timeout and payload-limit fields remain required as applicable.
| Retry mode | Unknown external outcome |
|---|---|
idempotency_key | Retry the same request with the same effect identity. The handler must actually pass the key to an idempotent service. |
reconcile | Stop with EffectReconciliationRequired until explicit reconciliation. |
none | Stop with EffectOutcomeUnknown; retry is not presented as safe. |
An external-write binding uses touba-tool-handler-v1. The SDK exposes
context.effect.effect_id and context.createEffectReceipt(...); the receipt
binds the outcome to the request identity and digest. Outcomes are applied,
not_applied and unknown. These helpers do not make the external service
idempotent.
Use a durable host
The CLI, SDK, project server and trigger service connect execution to a durable
effect journal. Direct CLI runs require --effect-journal <path.effect-journal.sqlite>;
ToubaEngine.run accepts the same path through effectJournal. A fresh run
gets a distinct scope; a human-checkpoint resume retains its run identity.
Rerunning a command is not an idempotent continuation. The CLI holds an
exclusive <journal>.lock; a crashed lease requires host inspection before
removal. Do not share this journal with another host. The journal alone is not
a continuation checkpoint. For the trigger service:
touba trigger serve program.air.json \
--manifest touba.manifest.json \
--state touba.triggers.sqlite \
--queue touba.trigger-queue.sqlite \
--effect-journal touba.effect-journal.sqlite
The journal records prepared, dispatched and the observed outcome. After an
interruption, a surviving dispatched state becomes unknown. An applied
effect with a restorable result is read without redispatch; without that result,
Touba refuses with EffectResultUnavailable. Reusing an identity with different
content produces EffectRequestConflict before the handler.
For webhooks, provide a stable x-touba-event-id. Identical pending deliveries
reuse the queue entry; a changed payload under that identity is refused with
TriggerEventPayloadConflict. A consumed event is refused with
TriggerReplayRejected. Without an explicit event identity, each delivery is
a new event.
Operating boundary
The provided journal is single-host, single-process storage. Do not share its file across processes or machines. It is not a distributed transaction or a universal exactly-once guarantee. An external service remains responsible for its own persistence and idempotency. Handlers remain host code to audit.
See Durable resume and Explicit compensation.
Handler error and effect outcome
A business error retains its public code even when its receipt is missing.
The separate receipt_error_code diagnostic describes the missing or invalid
receipt. The effect outcome stays unknown until a valid receipt or
reconciliation establishes it.
After a confirmed rollback, a Python handler can report:
from touba_sdk import ToubaHandlerError, create_effect_receipt
raise ToubaHandlerError(
"ExternalWriteRefused",
"The database refused the transaction.",
{"effect_receipt": create_effect_receipt(
context["effect"], {"outcome": "not_applied"}
)},
)
JavaScript equivalent, after a confirmed rollback:
throw new ToubaHandlerError("ExternalWriteRefused", "The database refused the transaction.", {
effect_receipt: context.createEffectReceipt({ outcome: "not_applied" }),
});
Use unknown if the commit might have succeeded. Keep connection details,
SQL queries and private data out of the public message.