Guides

Authoring digests and briefs

The read-side recipe pattern — fan-in reads, deterministic aggregation, rendered tables, and a notification that only fires when there is something to say.

A digest reads several sources, computes over them, and renders a compact answer — a morning brief, a pipeline cockpit, a renewal radar. It is the most common shape in the marketplace, and the discipline is the same at every size:

text
read → normalize → derive → render → notify (only on findings)

A digest is not a special kind of recipe. The same JSON runs when you click Run, when chat calls it, and when a schedule fires it — the pattern is about what the steps do. Slugs and fields below are illustrative.

#Keep it read-only

Everything a digest does against your accounts is a read. The only writes that belong inside one are a notification and, when the digest surfaces work to act on, a button section whose actions open another recipe — a normal, still-governed run with the arguments prefilled. Anything that changes a record other people see belongs in that second, deliberately-run recipe, not in the digest.

Say so in the description ("Read-only. It never creates, updates, or cancels anything") — installers read it, and so does the model when chat picks tools.

#The read layer

Prefer sources in this order:

  1. Enrichments. Precomputed facts — rollups, health scores, silence durations — are already maintained for you; reading one is instant. See the warehouse.
  2. Warehouse reads. Mail, calendar, contacts, files, timelines via built-in operations.
  3. Vendor operations. From an installed pack (listed in depends_on), with the account chosen by the user through a connection variable:
json
"variables": {
  "acct": { "label": "Accounting connection", "type": "connection",
            "connection_kind": "api", "default": "" }
},
"steps": [
  { "id": "invoices", "op": "core.acct.invoice.search",
    "connection": "{{config.acct}}" }
]

Canonical families like the one above dispatch to whichever vendor the chosen connection binds — one recipe serves QuickBooks and Xero alike.

Normalize immediately after every read, so downstream steps see one safe shape even when a source returned nothing:

json
{ "id": "invoices_safe", "transform": "default",
  "value": "{{step.invoices.result}}", "fallback": [] }

That fallback is for display math only. If a decision hangs on the value — a notification gate, a proposed action — check is_not_empty explicitly first; an empty fallback would silently disable the decision.

#The derive layer

Counts, totals, overdue flags, and rankings are transforms, not AI:

json
{ "id": "open", "transform": "filter", "array": "{{step.invoices_safe}}",
  "field": "balance", "operator": "greater", "value": 0 },
{ "id": "open_dated", "transform": "map", "array": "{{step.open}}",
  "apply": "is_past", "field": "due_date", "output_field": "overdue" },
{ "id": "open_total", "transform": "reduce", "array": "{{step.open}}",
  "field": "balance", "operator": "sum", "initial": 0 },
{ "id": "ranked", "transform": "sort", "array": "{{step.open_dated}}",
  "fields": [{ "field": "balance", "direction": "desc" }] },
{ "id": "rows", "transform": "slice", "array": "{{step.ranked}}",
  "start": 0, "end": "{{config.row_limit}}" }

Always cap rendered rows with a user-configurable row_limit, and when you truncate, put the full count in the summary — a table that quietly shows 20 of 400 reads as "everything is fine."

Reach for an AI step only when the digest genuinely needs synthesis — a prose "so what" over the assembled numbers. Most digests need none.

#The render layer

Route results through output.render: the headline numbers first (to_summary), one table per question (to_table), actions last. Format hints ({{step.total:currency}}, format: "relative" on date columns) apply inside rendered strings. The section types are in the recipe schema reference.

A digest that surfaces work should hand you the next step as a button, not as prose:

json
{ "type": "button", "source": "step.actions" }

Each action names another installed recipe with prefilled configuration. Clicking one opens its normal run — grants and approvals still apply.

#Notify only on findings

Pulling the digest (Run, chat, the panel) needs nothing extra. To push it:

json
{ "id": "should_notify", "transform": "all", "conditions": [
    "{{step.has_findings}} equal true",
    "{{config.notify_enabled}} equal true" ] },
{ "id": "notify", "skip_when": "{{step.should_notify}} not_equal true",
  "op": "core.notification.send",
  "args": { "channels": "{{config.channels}}",
            "title": "3 invoices overdue",
            "text": "Overdue total $4,210 across 3 invoices." } }

Let the user pick channels (in-app by default; email and connected chat platforms such as Slack or Telegram once set up), gate on "is there anything to say" plus an opt-out, and keep the text to headline numbers — the tables live in the result panel. Sending notifications requires the notification_send permission in the recipe's requires.

A daily "0 items" push trains its reader to mute it. Notify on signal, never on schedule.

#Running it on a cadence

Ship the digest manual-first and let the owner choose the cadence:

  • A schedule — armed in the UI or from chat. Each schedule carries its own configuration, so one installed digest can run daily for one client and weekly for another.
  • A self-ticking windowauto_run with a time-window gate, when the cadence is part of the recipe's meaning:
json
"auto_run": { "interval_ms": 3600000 },
"trigger_steps": [
  { "id": "window", "op": "core.watch.time",
    "args": { "weekdays": "{{config.weekdays}}",
              "start_hour": "{{config.start_hour}}",
              "end_hour": "{{config.end_hour}}" } }
]

Recipes with triggers install disarmed — the owner arms them deliberately. Set metadata.budget_ms on anything scheduled (15–35 seconds is typical for a digest that fans out across vendor calls), and see Triggers and watchers for everything about automated fires — including why outward-facing actions don't belong in them.

#Checklist

  • Every outside read normalized; decisions re-check emptiness explicitly.
  • Rows capped; truncation surfaced as a total.
  • Summary first, tables per question, actions as buttons to a second recipe.
  • Notification gated on findings and opt-in; channels user-chosen; notification_send declared.
  • Description states the read-only scope; budget_ms set.

Recued is local first; your server remains the authority.

Recued Docs

Search documentation

Start typing to search the documentation.