# `sandloom.sdk.sagas`

Sagas (ADR-0008): definitions, steps, start and status.

Public API 1.0 (spec §2.2, Appendix C).

`from sandloom.sdk.sagas import <name>`. Signatures in a terminal: `sandloom docs <name>`. Beginner names are the ones generated code and the examples use; any other name is advanced (mark its import line `# sandloom: advanced <reason>` in generated code and examples).

## Advanced

| Name | Kind | Summary |
| --- | --- | --- |
| [`CommandStep`](<#commandstep>) | class | A step that calls a command contract (`call="<contract>.<method>"`). |
| [`Direction`](<#direction>) | callable |  |
| [`local_step`](<#local_step>) | function | A local step: `await apply(db, ctx)` inside the transition's UoW. |
| [`LocalStep`](<#localstep>) | class | A step run in the owner's own Unit of Work (transactional, §1). |
| [`Saga`](<#saga>) | class | A saga definition (ADR-0008), discovered from the module's `sagas` submodule; every parameter is today's. |
| [`saga_contract_requirements`](<#saga_contract_requirements>) | function | One `ContractRequirement(name, methods=...)` per contract it calls. |
| [`saga_status`](<#saga_status>) | async function | The instance of `definition` for `saga_id` (`None` if never started). |
| [`saga_step_key`](<#saga_step_key>) | function | The idempotency key the runtime injects into a step's command call. |
| [`SagaContext`](<#sagacontext>) | class | What a step's argument function (or local step) sees. |
| [`SagaHandle`](<#sagahandle>) | class | What `start_saga` / `saga_status` return. |
| [`SagaInputConflict`](<#sagainputconflict>) | exception | `start_saga` of an existing business id with OTHER input (409). |
| [`SagaStep`](<#sagastep>) | constant |  |
| [`start_saga`](<#start_saga>) | async function | Start `definition` for business id `saga_id` in the caller's UoW. |
| [`step`](<#step>) | function | A command step: `call` a command contract with `args(ctx)`: |
| [`StepKind`](<#stepkind>) | class | The kind of a saga step (Garcia-Molina &amp; Salem; Richardson). |
| [`synthetic_instance`](<#synthetic_instance>) | function | An instance of `model` built from its schema alone (no validation). |

## Advanced names

### `CommandStep`

```
class CommandStep(
    name: str,
    call: str,
    args: ArgsFn,
    compensate: str | None = None,
    compensate_args: ArgsFn | None = None,
    kind: StepKind | None = None,
    refuse_on: frozenset[int] = frozenset(),
    check_args: bool = True,
    deadline: timedelta | None = None,
) -> None
```

A step that calls a command contract (`call="<contract>.<method>"`).

#### `CommandStep.local`

```
local  (property)
```

#### `CommandStep.contract`

```
contract  (property)
```

#### `CommandStep.method`

```
method  (property)
```

#### `CommandStep.compensate_contract`

```
compensate_contract  (property)
```

#### `CommandStep.compensate_method`

```
compensate_method  (property)
```

#### `CommandStep.has_compensation`

```
has_compensation  (property)
```

#### `CommandStep.undo_args`

```
def undo_args(self) -> ArgsFn
```

The compensation's argument function (default: the forward one).

### `Direction`

```
def Direction(*args, **kwargs)
```

*(no docstring)*

### `local_step`

```
def local_step(
    name: str,
    *,
    apply: LocalFn,
    compensate: LocalFn | None = None,
    kind: StepKind | str | None = None,
    deadline: timedelta | None = None,
) -> LocalStep
```

A local step: `await apply(db, ctx)` inside the transition's UoW.

Its writes commit with the instance transition (transactional, no command log needed); raising rolls them back (a savepoint). Raise an exception with an `http_status` of 400/402/403/404/410/422 for a definitive refusal; anything else is retried. The returned value (JSON-able) is persisted as the step's result.

### `LocalStep`

```
class LocalStep(
    name: str,
    apply: LocalFn,
    compensate: LocalFn | None = None,
    kind: StepKind | None = None,
    deadline: timedelta | None = None,
) -> None
```

A step run in the owner's own Unit of Work (transactional, §1).

#### `LocalStep.local`

```
local  (property)
```

#### `LocalStep.has_compensation`

```
has_compensation  (property)
```

### `Saga`

```
class Saga(Saga)(
    name: str,
    *,
    version: int,
    input: type[InputT],
    deadline: timedelta,
    steps: Sequence[SagaStep],
    compensation_deadline: timedelta | None = None,
) -> None
```

A saga definition (ADR-0008), discovered from the module's `sagas` submodule; every parameter is today's.

### `saga_contract_requirements`

```
def saga_contract_requirements(saga: Saga[Any]) -> list[ContractRequirement]
```

One `ContractRequirement(name, methods=...)` per contract it calls.

`register_saga` adds these to the module's `consumes`, so a missing provider is a `ContractBindingError` at startup like any consumed contract, and the rolling-upgrade check covers exactly these methods.

### `saga_status`

```
async def saga_status(definition: Saga[Any], saga_id: object) -> SagaHandle | None
```

The instance of `definition` for `saga_id` (`None` if never started).

### `saga_step_key`

```
def saga_step_key(saga_type: str, saga_id: str, step: str, direction: Direction) -> str
```

The idempotency key the runtime injects into a step's command call.

`saga:<saga_type>:<saga_id>:<step>:do` (forward) or `…:undo` (compensation) — stable across attempts, so the provider's durable command log replays a step that already ran instead of running it twice.

### `SagaContext`

```
class SagaContext(Generic)(
    saga_type: str,
    saga_id: str,
    input: InputT,
    results: Mapping[str, Any] = <factory>,
    synthetic: bool = False,
) -> None
```

What a step's argument function (or local step) sees.

`input` is the definition's input model; `results` maps each applied step's name to its persisted (JSON) result. `key(step)` is the derived operation key of a step (e.g. to pass the charge's key to the refund); `result(step, Model)` decodes a step's result to `Model`.

#### `SagaContext.key`

```
def key(self, step: str, direction: Direction = 'do') -> str
```

The operation key the runtime uses for `step` (`do` / `undo`).

#### `SagaContext.result`

```
def result(self, step: str, model: Any = None) -> Any
```

The result `step` returned, decoded to `model` when given.

Raises `KeyError` for a step that has not been applied (yet).

### `SagaHandle`

```
class SagaHandle(
    saga_type: str,
    saga_id: str,
    instance_id: uuid.UUID,
    status: str,
    created: bool = False,
    step: str | None = None,
    park_reason: str | None = None,
    version: int = 0,
) -> None
```

What `start_saga` / `saga_status` return.

### `SagaInputConflict`

```
class SagaInputConflict(RuntimeError)
```

`start_saga` of an existing business id with OTHER input (409).

### `SagaStep`

```
SagaStep: UnionType
```

*(no docstring)*

### `start_saga`

```
async def start_saga(
    definition: Saga[Any],
    *,
    saga_id: object,
    input: BaseModel | Mapping[str, Any],
) -> SagaHandle
```

Start `definition` for business id `saga_id` in the caller's UoW.

Inserts the instance row and publishes its first nudge through the outbox in the SAME transaction as the business change. Starting the same `saga_id` again is a create-or-get: equal input returns the existing instance (`created=False`), other input raises `SagaInputConflict` (409). Outside a Unit of Work it raises `UnitOfWorkRequiredError`, like `publish_event`.

### `step`

```
def step(
    name: str,
    *,
    call: CommandRef,
    args: ArgsFn,
    compensate: CommandRef | None = None,
    compensate_args: ArgsFn | None = None,
    kind: StepKind | str | None = None,
    refuse_on: Iterable[int] = (),
    check_args: bool = True,
    deadline: timedelta | None = None,
) -> CommandStep
```

A command step: `call` a command contract with `args(ctx)`:

```
credits = Contract(BILLING_CREDITS_V1)
step("charge", call=credits.spend, args=lambda ctx: {...},
     compensate=credits.grant)
```

Identical to the L2 `step` with `call="billing.credits.v1.spend"`.

### `StepKind`

```
class StepKind(StrEnum)
```

The kind of a saga step (Garcia-Molina &amp; Salem; Richardson).

### `synthetic_instance`

```
def synthetic_instance(model: type[T]) -> T
```

An instance of `model` built from its schema alone (no validation).

Defaults are kept; required fields get a representative value of their type (`1`, `"x"`, `UUID(int=1)`, the first enum member, a nested synthetic model, an empty collection). Used to run a step's `args` function once at startup (`check_args`); a function that cannot run on such input opts out per step with `check_args=False`.

## Examples and related guides

- [Add a saga](<https://sandloom.dev/docs/modules/between/add-saga/index.md>)
- [Python API overview](<https://sandloom.dev/docs/reference/sdk/index.md>)
