# `Cairnloop.Outbound.BulkEnvelope`
[🔗](https://github.com/szTheory/cairnloop/blob/main/lib/cairnloop/outbound/bulk_envelope.ex#L1)

Durable audit envelope for a bulk outbound action (D-13, OBS-02-shaped). Snapshots
template_id, rendered_body, and recipient cohort at confirmation time. Per-recipient
delivery flows through `Outbound.trigger/2` (sealed) carrying the envelope id. Refused
attempts persist with `status: :refused_cap_exceeded`.

## Columns

- `:id` — UUID generated by the caller at `bulk_trigger/2` confirmation time
  (`autogenerate: false` — callers must supply an explicit `Ecto.UUID.generate/0`
  value so tests and downstream correlation keys can assert on it).
- `:template_id` — string id of the configured template that was used at confirmation.
- `:rendered_body` — the fully rendered message body at confirmation time. **Never**
  re-rendered at worker run time (CLAUDE.md: snapshot trust facts at decision time).
- `:recipient_conversation_ids` — list of `:integer` conversation ids the operator
  targeted. Persisted for audit (OBS-02); per-recipient delivery is tracked on the
  individual `system_outbound` `Message` rows created via the sealed
  `Outbound.trigger/2` lane.
- `:count` — denormalized cohort size (must be `> 0`).
- `:effective_cap` — the value of `max_batch_size/0` at decision time. Snapshotted
  on BOTH the `:submitted` and `:refused_cap_exceeded` paths so OBS-02 readers can
  compare `count` against the policy that was actually in effect when the operator
  confirmed (WR-05). Must be `> 0`.
- `:requested_by` — actor string (operator, system, etc.); nullable so system-initiated
  bulk actions are representable.
- `:requested_at` — confirmation timestamp.
- `:status` — `:submitted` (default) or `:refused_cap_exceeded`. A refused row records
  that an oversized cohort was attempted — mirrors `Governance.propose_blocked`'s
  "blocked attempts persist" posture so OBS-02 reads see both lanes from one table.
- `:refused_reason` — operator-facing reason string (only set when refusing).

## Status enum

    :submitted              — fan-out enqueued (per-recipient delivery is tracked on Message rows)
    :refused_cap_exceeded   — cohort exceeded max_batch_size; no fan-out occurred

## Validation

- `:id`, `:template_id`, `:rendered_body`, `:recipient_conversation_ids`, `:count`,
  `:effective_cap`, and `:requested_at` are required.
- `:count` must be `> 0` (a zero-cohort envelope has no audit value).
- `:effective_cap` must be `> 0`.

See `priv/repo/migrations/20260527063000_add_outbound_bulk_envelopes.exs` for the
underlying table definition (D-15).

# `changeset`

Builds a changeset for the bulk envelope.

Casts all writable fields and validates required ones (`:id`, `:template_id`,
`:rendered_body`, `:recipient_conversation_ids`, `:count`, `:effective_cap`,
`:requested_at`). Enforces `count > 0` and `effective_cap > 0` (zero values
have no audit value).

`:requested_by` is intentionally optional — system-initiated bulk actions are
representable with `nil` here. `:status` defaults to `:submitted`; setting it to
`:refused_cap_exceeded` with a `:refused_reason` is also valid.

---

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