> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0397 — Actions remainder: effect codes, the tool default, reserve-then-commit, the audit in the unit of work and an approval step that fails closed

> Proposed · 2026-10-26

> [!NOTE]
>
> Proposed. It settles the open rows of `@stackra/actions` after wave 10 and touches
> [0099](https://docs.stackra.figentra.com/adr/0099-risk-scale-l0-to-l4.md) (decision 2), [0138](https://docs.stackra.figentra.com/adr/0138-pipeline-order-is-fixed.md),
> [0212](https://docs.stackra.figentra.com/adr/0212-audit-evidence-access-logs-hash-chain-and-retention.md) (decision 4),
> [0215](https://docs.stackra.figentra.com/adr/0215-entitlements.md) and
> [0360](https://docs.stackra.figentra.com/adr/0360-the-ai-chain-four-layers-one-spine-approval-modes-and-the-never-bypass-floor.md)
> (decision 4). The amendments of 0137 and 0138 themselves are written by the ADR group. Linear
> STA-765. Decision 5 amended by STA-813 (2026-10-26): an unbound approval step does not run; only
> an action that declares `approval: true` fails closed.

## Context

Wave 10 left five gaps in the action pipeline. The effect tags existed as an enum in `@stackra/actions/contracts`
with snake\_case codes while gen's list and hints spelt `external-communication` in kebab case (ADR 0099 was written
before ADR 0231). The runtime recorded `tool: false` for every action while gen, following ADR 0360 decision 4, makes
an action a user can perform a tool unless it says `tool: false` with a reason. Plan limits were checked by the
entitlement step but nothing serialised two creates racing at the limit (owner round 3 item 18). The audit entry was
written after the unit of work committed, so a crash between the two lost it. And with no `ApprovalGate` bound the
approval step was skipped, so an app that forgot the binding ran every call unapproved.

## Decision

1. **Effect codes are snake\_case.** `Effect` (`financial`, `security`, `pii`, `external_communication`, `irreversible`,
   `bulk`) is the one vocabulary, exported from `@stackra/actions/contracts` (ADR 0231 wins over the kebab spelling of
   ADR 0099). An action declares its tags with `effects: ActionEffects` (distinct `Effect` values); gen's registered
   tags win at run time and reach the call descriptor. Gen's `EFFECT_TAGS` and tool hints adopt the same codes.
2. **The tool default is gen's.** At run time an action whose actors include a user is a tool unless it says
   `tool: false` (with the `// reason:` line gen reads); any other action is not. A query stays a tool by default.
3. **Reserve, then commit.** A `LimitReservation` port wraps every applied action's unit of work: the driver
   (`@stackra/entitlements`, over `LimitReservationService`) holds the limit's lock for a call that creates a counted
   model, checks the grant and the count, runs the commit and releases. Which call counts against which limit is the
   entitlements registry, never a verb the pipeline parses. No binding means no plan limits; callers write no
   reservation code.
4. **The success audit is in the unit of work.** A successful action's audit entry is written inside its transaction,
   after `handle` and its usage event, so it commits or rolls back with the writes. A refusal's entry (its writes
   rolled back) and an idempotent replay's (no unit of work ran) are written after, outside.
5. **Approval fails closed where it is declared** (as amended by STA-813). The approval step runs only when an
   `ApprovalGate` is bound; with none bound an app has no approvals and the step does not run. An action that declares
   `approval: true` must pass a gate, so a call of it with no gate bound is an error, like a call with no
   `AccessControl`. `OpenApprovalGateDriver` remains for an app that binds a gate on purpose and wants every call to
   pass it. A query is never parked. The first text (every action fails closed with no gate) broke every pipeline
   built without one: the iam, entitlements and queue tests, the queue worker and the workflow runner.

## Consequences

* A service whose tenants take approval rules binds the OS tasks-approvals driver; every other pipeline (the queue
  worker, the workflow runner, the iam, entitlements and queue test suites) binds nothing and keeps working. The risk
  that an app forgets the binding is carried by the actions that matter declaring `approval: true`, which fail closed.
* An action a user can perform appears in the tool table and at run time alike; a user-only action that must not be
  a tool says so with a reason.
* A long transaction now also holds the limit's lock; the hold time is the service's default (30 seconds).
* Gen's effect list changes spelling; manifests regenerate.

## Enforcement

`packages/application/actions/src/core/services/__tests__/action-pipeline.service.test.ts` (an unbound gate skips the step,
`approval: true` fails closed, reservation around the unit of work, audit order); `packages/application/actions/src/core/bases/__tests__/action.base.test.ts`
(tool default, effects); `packages/security/entitlements/src/core/drivers/__tests__/plan-limit-reservation.driver.test.ts`
(the entitlements `LimitReservation` driver, ADR 0408). Planned — the gen change of `EFFECT_TAGS` and its conformance
test.

## Supersedes and sources

Supersedes: none. Sources: STA-765; owner round 3 items 18 and 33; `notes/handover/stackra-package-needs.md`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.