# `Enact.Action`
[🔗](https://github.com/svycal/enact/blob/v0.1.0/lib/enact/action.ex#L1)

The behaviour every action module implements.

`use Enact.Action` sets `@behaviour Enact.Action` and overridable
defaults for the optional callbacks — that is its entire body; there is
no DSL and no code generation.

Required: `input/0`, `authorize/1`, `execute/2`. An open write is a
written `authorize/1` returning `true`, never an omitted callback.

Every callback is either pure data (`config/0`, `input/0`, `resolvers/0`)
or a single-purpose function over `(changeset | params, ctx)`. The runner
(`Enact.run/3`) orchestrates them as:

    load → authorize → cast → validate → resolve → execute → after_commit

# `after_commit`
*optional* 

```elixir
@callback after_commit(result :: term(), ctx :: Enact.Context.t()) :: term()
```

Post-commit side effects (job insertion, analytics). Runs strictly after
a successful commit, outside the transaction; its return value is
ignored and it never rolls back. A raise propagates to the caller, but
the write is already committed and the success telemetry event has
already been emitted — the audit trail records the write either way.

# `authorize`

```elixir
@callback authorize(ctx :: Enact.Context.t()) :: boolean() | {:error, term()}
```

Authorizes the actor against the loaded context. Required — a forgotten
policy is indistinguishable from an open write. Runs before cast —
unauthorized callers trigger no input work.

Return values:

  * `true` — proceed
  * `false` — `{:error, %Enact.Error{type: :forbidden}}`
  * `{:error, reason}` — `{:error, %Enact.Error{type: :forbidden, reason: reason}}`
    (`{:error, :foo}` → `{:error, %Enact.Error{type: :forbidden, reason: :foo}}`)
  * `{:error, %Enact.Error{}}` — passed through

`reason` is for logs, telemetry, and the host renderer. Do not echo it
wholesale. Match host-owned atoms in the renderer when the 403 copy
should vary; leave the default opaque.

An open write (any non-anonymous actor; or anyone, on `anonymous?: true`
actions) is a written `true`, never an omitted callback.

# `config`
*optional* 

```elixir
@callback config() :: keyword()
```

Static action configuration.

Recognized keys (all optional): `mode: :create | :patch` (default
`:create`), `anonymous?: boolean` (default `false`).

# `execute`

```elixir
@callback execute(changeset :: Ecto.Changeset.t(), ctx :: Enact.Context.t()) ::
  {:ok, term()} | {:error, term()}
```

Persists the write. Runs inside `repo.transaction/1`; `{:error, reason}`
rolls back. A returned `%Ecto.Changeset{}` error (declared constraints)
is promoted to `:invalid`; any other error becomes `:internal`.

# `input`

```elixir
@callback input() :: module() | nil
```

The input-schema module (`Enact.InputSchema`), or `nil` for input-less
actions (archive, cancel, resend) — the runner then skips cast and
validate, and `Enact.updates/2` returns `%{}`.

# `load_subject`
*optional* 

```elixir
@callback load_subject(params :: map(), ctx :: Enact.Context.t()) ::
  struct() | nil | :no_subject | {:error, term()}
```

Fetches the subject — the URL-anchored record the action operates on or
within (the updated record in patch mode; the parent in
create-under-parent). Always invoked. The default is a no-op that
returns `:no_subject` (leave `ctx.subject` nil). Returning `nil`
produces `:not_found`. Must scope by the trust anchor (§9 of the
design spec). Params are string-keyed — the runner stringifies atom
keys at the `run`/`dry_run`/`subject`/`authorized` boundary — and values are uncast. Match
`%{"id" => id}`, not `params[:id]`.

# `resolvers`
*optional* 

```elixir
@callback resolvers() :: keyword()
```

Declarative reference-resolution spec: `[name: {field_or_path, fetcher}]`.
See `Enact.Resolve`.

# `validate`
*optional* 

```elixir
@callback validate(changeset :: Ecto.Changeset.t(), ctx :: Enact.Context.t()) ::
  Ecto.Changeset.t()
```

Ordinary changeset pipeline for operation-specific rules (payload-
intrinsic rules belong in the input module's own validations). Not
invoked for `input: nil` actions.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
