Saga-Pattern Transactions in Effect Golem Agents
SkillAI & modelsAdds multi-step saga transactions with automatic rollback to your agent's workflows so failed operations undo earlier steps.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Saga-Pattern Transactions in Effect Golem Agents skill
About this capability
Adding Effect-based saga transactions with compensating rollback to a Golem agent. Use when an @golemcloud/effect-golem project needs fallible multi-step operations, reverse-order compensation, rollback reporting, or durable saga recovery.
What this skill tells your AI
The instructions your AI receives, as published by golemcloud/golem in golem-skills/skills/effect/golem-add-transactions-effect/SKILL.md and read by ahel’s review.
Use the Saga namespace from @golemcloud/effect-golem. A saga is an ordinary Effect program:
each successful step registers an Effect that can compensate it, and the transaction wrapper runs
registered compensations in reverse order when the body fails.
import { Cause, Effect, Exit, Schema } from "effect";
import {
FetchHttpClient,
HttpClient,
HttpClientRequest,
HttpClientResponse,
} from "effect/unstable/http";
import { method, Saga } from "@golemcloud/effect-golem";
Do not import transaction helpers from @golemcloud/golem-ts-sdk; its Promise/Result API is not
the Effect SDK API.
Choose the Compensation API
The Effect SDK provides three ways to define a step:
Saga.withCompensation(effect, compensate)registers an infallible compensation whose error channel isnever.Saga.withFallibleCompensation(effect, compensate)allows compensation to fail. A failed rollback becomesFailedAndRolledBackPartially; remaining compensations still run.Saga.operation({ execute, compensate })creates a reusable(input) => Effectstep. The compensation receives(input, output, cause), but it must be infallible.
Use withFallibleCompensation for outgoing HTTP calls because a remote compensation can fail in
the HTTP client's typed error channel. Do not hide that error merely to satisfy
Saga.operation's never requirement.
Assuming these application functions already return Effects, a reusable infallible operation looks like this:
const reserveInventory = Saga.operation({
execute: (sku: string) => reserve(sku),
compensate: (_sku, reservation, _cause) => release(reservation.reservationId),
});
The compensation is registered only after execute succeeds. A failing execute path therefore
does not compensate itself.
Fallible Transactions
Wrap the complete sequence in Saga.fallibleTransaction. Signal expected failures that should
trigger rollback with Effect.fail, not throw, Effect.die, or Effect.dieMessage:
const transaction = Saga.fallibleTransaction(
Effect.gen(function* () {
const reservation = yield* reserveInventory("SKU-123");
const charge = yield* chargePayment(4999);
if (mustRollback) {
return yield* Effect.fail("requested-rollback" as const);
}
return { reservation, charge };
}),
);
A typed failure returns one of these through the Effect error channel:
type TransactionFailure<E> =
| { readonly _tag: "FailedAndRolledBackCompletely"; readonly error: E }
| {
readonly _tag: "FailedAndRolledBackPartially";
readonly error: E;
readonly compensationError: unknown;
};
fallibleTransaction can also fail with SDK host errors or NestedSagaError. If an agent method
returns a business-result DTO, convert only the two transaction-failure tags into that DTO. Do not
misreport unexpected host failures as a successful rollback.
External HTTP Ledger Pattern
Use Effect's HttpClient when saga execute and compensation steps call an external service. The
caller supplies a stable requestId, and each logical action gets its own idempotency key so replay
or compensation retries do not duplicate that action. The external service must enforce those
keys; headers alone do not provide deduplication.
This handler performs two HTTP steps and reports rollback as a ProcessOrderResult:
const ProcessOrderInputSchema = Schema.Struct({
requestId: Schema.String,
orderId: Schema.String,
failAfterCharge: Schema.Boolean,
});
const ProcessOrderResultSchema = Schema.Struct({
success: Schema.Boolean,
error: Schema.NullOr(Schema.String),
});
type ProcessOrderInput = typeof ProcessOrderInputSchema.Type;
type ProcessOrderResult = typeof ProcessOrderResultSchema.Type;
// Add this entry to the agent's methods record.
const processOrderMethod = method({
input: { input: ProcessOrderInputSchema },
success: ProcessOrderResultSchema,
});
const ledgerCall = (
action: "reserve" | "release" | "charge" | "refund",
orderId: string,
requestId: string,
) =>
HttpClientRequest.post(`https://ledger.example.com/orders/${action}`).pipe(
HttpClientRequest.setHeader("Idempotency-Key", `${requestId}:${action}`),
HttpClientRequest.bodyJson({ orderId }),
Effect.flatMap(HttpClient.execute),
Effect.flatMap(HttpClientResponse.filterStatusOk),
Effect.asVoid,
);
const processOrder = ({
requestId,
orderId,
failAfterCharge,
}: ProcessOrderInput) =>
Effect.gen(function* () {
const exit = yield* Effect.exit(
Saga.fallibleTransaction(
Effect.gen(function* () {
yield* Saga.withFallibleCompensation(
ledgerCall("reserve", orderId, requestId),
() => ledgerCall("release", orderId, requestId),
);
yield* Saga.withFallibleCompensation(
ledgerCall("charge", orderId, requestId),
() => ledgerCall("refund", orderId, requestId),
);
if (failAfterCharge) {
return yield* Effect.fail("requested-rollback" as const);
}
return "committed" as const;
}),
),
);
if (Exit.isSuccess(exit)) {
return { success: true, error: null } satisfies ProcessOrderResult;
}
const failReason = exit.cause.reasons.find(Cause.isFailReason);
const failure = failReason?.error;
if (
typeof failure === "object" &&
failure !== null &&
"_tag" in failure &&
(failure._tag === "FailedAndRolledBackCompletely" ||
failure._tag === "FailedAndRolledBackPartially")
) {
const rollback = failure as Saga.TransactionFailure<unknown>;
const error =
rollback._tag === "FailedAndRolledBackPartially"
? `${String(rollback.error)}; compensation: ${String(rollback.compensationError)}`
: String(rollback.error);
return { success: false, error } satisfies ProcessOrderResult;
}
return yield* Effect.die(exit.cause);
}).pipe(Effect.provide(FetchHttpClient.layer));
Put processOrder: processOrderMethod in the agent definition's methods record. The one
top-level parameter named input is intentional: it lets callers provide the three fields as one
TypeScript record value. The implementation receives the named-parameter wrapper, so expose the
handler as follows:
processOrder: ({ input }) => processOrder(input),
The handler must return that Effect, and every successful branch of the Effect must return a
ProcessOrderResult. Do not declare success: Schema.Void, drop the result with Effect.tap, or
yield the mapping Effect and then fall through. A void method omits result_json from CLI JSON
output.
The CLI accepts one shell positional per top-level method parameter. With the single input record
above, invoke it with one record positional:
golem agent invoke 'CounterAgent("main")' processOrder \
'{ requestId: "request-1", orderId: "order-1", failAfterCharge: true }'
If instead a method declares
input: { requestId: ..., orderId: ..., failAfterCharge: ... }, pass three separate shell
positionals. Do not combine multiple top-level parameters into one comma-separated argument.
For rollback order refund then release, register the reserve step first and the charge step
second. Execute every request with HttpClient.execute, reject non-2xx responses with
HttpClientResponse.filterStatusOk, and provide FetchHttpClient.layer around the composed
handler Effect.
Infallible Transactions
Saga.infallibleTransaction(body) accepts only Effect<A, never, R>. On interruption it drains
compensations, rewinds to the transaction checkpoint with the Golem oplog, and relies on durable
replay to try again. Do not cast away a real error channel or turn expected failures into defects to
force a body into this API. A deterministic retry condition can loop forever, and rewind recovery
must not be wrapped in persistNothing.
Use fallibleTransaction whenever failure is an acceptable caller-visible outcome. It is the right
choice for methods returning { success, error }.
Failure and Durability Rules
- Keep execute and compensation actions idempotent; durable replay or interruption can repeat work.
- Compensations run sequentially in reverse registration order.
- Await every step by yielding its Effect. Starting work outside the returned Effect breaks ordering.
- Defects bypass compensation and trap the worker. Use the typed error channel for business failure.
- Do not nest
fallibleTransactionorinfallibleTransaction; nested sagas fail withNestedSagaError. - Do not use compensation combinators outside a transaction; the step runs but no rollback is retained.
- Saga atomic regions make each durable step recoverable; they do not make external systems ACID.
- Await every outgoing HTTP execute and compensation request; do not start either one in a detached Effect.
- Keep the agent durable and use the default durable persistence level for oplog recovery.
Authoritative API Sources
Signals
- GitHub stars
- 2k
- Forks
- 211
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
golem-add-transactions-effect- Source
- github.com/golemcloud/golem