An entry made of the inbox's changes (Tayori.Inbox): the changes
waiting, for the public, put in an entry for the people who use the app.
{:ok, entry} = Tayori.Digest.run()
{:ok, entry} = Tayori.Digest.run(ids: ["0193…", "0194…"])The entry is a draft for the admin to read and publish, unless the
:digest config says publish: :publish; the changes it takes are
then "drafted", with the entry's id. A change for the team only
(audience: "internal"), or put aside, never goes into one.
Written by the model, or plain
When the admin has set up AI (Tayori.AI.configured?/0), the model
writes the entry from the changes: a title, a summary and a body, in
the first language and the product's voice, the highlights first. It
gets each change's title, body, category and highlight, never its ref,
link or author, and never a change for the team. With
digest: [translate: true], it also translates the entry into the
other languages. When the model fails, the digest is plain, and a
warning is logged.
A plain digest, without AI or with ai: false: one change makes an
entry of its own, its title and its body; several make an entry titled
with the day, with a section for each category, the highlighted first.
The entry's categories and segments are always the changes', not the model's.
When
The :every of the :digest config: :manual, when the admin asks;
:ingest, each time changes come (after_ingest/1); or a schedule,
which Tayori's scheduler keeps (next_run/3). A scheduled digest with
fewer changes than :minimum makes nothing, and its changes wait for
the next time. With the :oban option, it runs in a job of
Tayori.Worker; without, in a task. status/0 says where it stands.
Summary
Types
A schedule: the :every and :at of the :digest or :emails config.
Functions
Tells the digest that changes came: with every: :ingest, a digest
runs when at least one is new. Tayori.Inbox.ingest/0 calls it, and
so does whatever else adds changes in batches, with the count of new
ones.
Whether the scheduled digest of the config is due at a time: its
next_run/3 after the last one has come.
The time of the next scheduled run, after the last one (or after
now, when there was none), or nil when the schedule has no time
Makes an entry of the changes waiting. Options
Where the digest stands, for the admin: its :every, the time of the
next scheduled one (nil for :manual and :ingest), the last
scheduled one (nil before the first), and how many changes wait.
Types
Functions
@spec after_ingest(non_neg_integer()) :: :ok
Tells the digest that changes came: with every: :ingest, a digest
runs when at least one is new. Tayori.Inbox.ingest/0 calls it, and
so does whatever else adds changes in batches, with the count of new
ones.
@spec due?(Tayori.Config.t(), DateTime.t()) :: boolean()
Whether the scheduled digest of the config is due at a time: its
next_run/3 after the last one has come.
@spec next_run(schedule(), DateTime.t() | nil, DateTime.t()) :: DateTime.t() | nil
The time of the next scheduled run, after the last one (or after
now, when there was none), or nil when the schedule has no time:
{:weekly, day}- the first such day after the last run, at:at;:monthly- the first day of the month after the last run, at:at;{:days, n}- n days after the day of the last run, at:at;:manual,:ingest(and the:eachof:emails) - nil.
A time that is past is due: a run missed while the app was down is made once, at the next look.
Examples
iex> schedule = %{every: {:weekly, :monday}, at: ~T[09:00:00]}
iex> Tayori.Digest.next_run(schedule, ~U[2026-10-19 09:00:30Z], ~U[2026-10-20 12:00:00Z])
~U[2026-10-26 09:00:00Z]
iex> schedule = %{every: :monthly, at: ~T[09:00:00]}
iex> Tayori.Digest.next_run(schedule, nil, ~U[2026-10-20 12:00:00Z])
~U[2026-11-01 09:00:00Z]
iex> Tayori.Digest.next_run(%{every: :manual, at: ~T[09:00:00]}, nil, ~U[2026-10-20 12:00:00Z])
nil
@spec run(keyword()) :: {:ok, Tayori.Entry.t()} | {:error, :nothing | Ecto.Changeset.t()}
Makes an entry of the changes waiting. Options:
:ids- these changes only (still only the new, public ones).:since,:until- the changes that happened in this time.:publish-:draftor:publish, instead of the config's.:ai-falsefor a plain digest, even with AI set up.:now- the time of the digest (now).:author- the entry's author, as the resolver'sauthor/1gives it.
Returns {:ok, entry}, {:error, :nothing} when no change is
waiting, or {:error, changeset}.
@spec status() :: %{ every: term(), next_run_at: DateTime.t() | nil, last_run_at: DateTime.t() | nil, pending: non_neg_integer() }
Where the digest stands, for the admin: its :every, the time of the
next scheduled one (nil for :manual and :ingest), the last
scheduled one (nil before the first), and how many changes wait.