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

# Daily Digest

> A cron-triggered, read-only analyst that compiles what shipped in the last 24 hours and posts a one-line summary with the full digest threaded in Slack.

This example schedules a reporter. Every morning a heartbeat trigger spawns a session that reads the last 24 hours of merged PRs and direct-to-main commits, checks whether they deployed cleanly, and writes a structured digest — delivered as the run's report and, when Slack is connected, as one summary line in the channel with the full digest threaded beneath it.

Use it when "what shipped yesterday?" is a question someone answers by scrolling, and you want the answer produced on a clock by an agent that is incapable of changing anything.

## How it works

```mermaid theme={null}
flowchart LR
    A["Heartbeat schedule<br/>cron: 0 8 * * *"] -->|tick| B["Spawn digest session"]
    B --> C["git log + PR search +<br/>CI runs over the exact<br/>24h window"]
    C --> D["Run report"]
    C --> E["Slack: one summary line,<br/>digest threaded"]
```

A heartbeat trigger has no event key: it declares `kind: heartbeat` with a `cron` expression and an optional `timezone` (default `UTC`). At apply time, auto creates one durable Temporal schedule per heartbeat trigger; each tick records a normal internal event and routes it like any other. Only the declaring agent's heartbeat trigger can match its synthetic event key, and each tick is deduplicated by schedule and scheduled time, so missed-tick catch-ups never double-fire. See [cron and webhooks](/reference/events/cron-and-webhooks).

The tick payload is small but load-bearing:

```json theme={null}
{
  "trigger": "heartbeat",
  "heartbeat": {
    "scheduleId": "…",
    "agentResourceId": "…",
    "triggerOrdinal": 0,
    "scheduledAt": "2026-07-15T15:00:00.000Z"
  }
}
```

The prompt anchors the reporting window on `{{heartbeat.scheduledAt}}` — the *scheduled* time, not the moment the session happened to start — so the window stays exact even when a run starts late.

The other design decision is that the reporter is **read-only by construction**: the mount grants `contents: read`, the GitHub tool surface is the read/search set, and the instructions forbid running tests or builds. A reporter that cannot write cannot break anything.

## Install from the consolidated agent package

The packaged Ship Digest role is published in `@auto/agents`:

```yaml .auto/agents/ship-digest.yaml theme={null}
name: ship-digest
imports:
  - "@auto/agents@latest/ship-digest.yaml"
variables:
  repoFullName: acme/widgets
```

Slack delivery is zero-configuration: the template uses the standard `slack` connection name and `#dev` channel as `optional: true` wiring — apply skips it while no `slack` connection exists and activates it on the next apply once one does; until then the run report stands alone. Merge for [GitHub Sync](/concepts/github-sync) to apply.

## The full configuration

```text theme={null}
.auto/
  agents/ship-digest.yaml
  assets/ship-digest.png
  fragments/environments/agent-runtime.yaml
```

```yaml .auto/fragments/environments/agent-runtime.yaml theme={null}
harness: claude-code
environment:
  name: agent-runtime
  image:
    kind: preset
    name: node24
  resources:
    memoryMB: 8192
```

```yaml .auto/agents/ship-digest.yaml theme={null}
name: ship-digest
model:
  provider: anthropic
  id: claude-opus-4-8
identity:
  displayName: Ship Digest
  username: ship-digest
  avatar:
    asset: .auto/assets/ship-digest.png
  description: Daily shipped-code digest - summarizes merged work, flags follow-ups, and optionally posts the report to Slack.
imports:
  - ../fragments/environments/agent-runtime.yaml
systemPrompt: |
  You are a read-only code analyst for acme/widgets. You read code,
  history, and CI state, and you write reports; you never change anything.

  Analysis discipline:
  - Use explicit ISO timestamps in every git and GitHub query so time
    windows are exact.
  - Read deeply enough to describe what actually changed, not just titles:
    PR bodies and diffs via the pull_request_read tool, direct commits via
    git log on the mounted checkout.
  - Judge convention drift against the repo's written standards
    (CONTRIBUTING.md, style docs), not general taste.

  Hard limits: do not run tests, typechecks, builds, or dependency
  installs, and do not edit files or push commits. Your only permitted
  GitHub writes are the "Ship digest" tracking issue and its comments,
  and only under the explicit opt-in described in your delivery
  instructions - the default delivery is this run's report.

  Slack delivery is an optional zero-configuration pilot using the standard
  `slack` connection name and `#dev` channel. When the chat tool is available,
  mrkdwn links (<https://url|text>) and post one top-level summary with the
  full report threaded beneath it. When the tool is unavailable, do not
  treat Slack delivery as a failure; the run report remains the complete
  digest.
initialPrompt: |
  Produce the daily shipped-code digest for acme/widgets.

  This run was scheduled at {{heartbeat.scheduledAt}}. The reporting
  window is the 24 hours ending at that timestamp; compute the window start
  from it.

  Gather what shipped in the window:
  - merged PRs, with the search_pull_requests tool, query
    `repo:acme/widgets is:pr is:merged merged:>=<window-start-ISO>`;
    drop any whose merge timestamp falls outside the window
  - commits that landed directly on main:
    git log --since=<window-start-ISO> --until=<window-end-ISO> --first-parent HEAD
    The checkout is shallow and detached; if history does not reach the
    window start, run git fetch --shallow-since=<window-start-ISO> origin main
    first so the scan does not under-report.
  - for each merged PR, read the body, author, commits, and diff with
    pull_request_read (methods `get`, `get_commits`, and `get_diff`)
    deeply enough to describe what changed. Mark a PR as agent-authored only
    when its author or commit attribution provides direct evidence; otherwise
    label authorship unknown rather than guessing.
  - CI sessions on main in the window, with the actions_list tool, to say
    whether what merged actually deployed and to flag failed sessions

  Write the digest with these sections:
  1. Shipped - one entry per merged PR or direct commit; a line for
     mechanical changes, a short paragraph for substantial ones. Link each
     PR, note confirmed agent authorship when evidence exists, and say whether
     the day's merges deployed cleanly.
  2. Suggested follow-ups - concrete work the shipped changes imply:
     missing tests, TODOs introduced, docs that now lag the code.
  3. Quality watch - anything drifting from the repo's written conventions,
     citing the PR and file; write "No drift observed." when clean.
  4. In flight - open PRs (search_pull_requests, `is:pr is:open`), one line
     each.

  Deliver the digest as this run's report: your final message is the
  digest, opening with the report date. The run is read later from the
  project's sessions view, so write it to stand on its own. If nothing
  shipped, still produce the report - the in-flight and watch sections
  remain useful.

  When the chat tool is available, also send exactly one Slack message with
  chat.send to target provider `slack`, destination channel `#dev`: a single
  sentence summarizing the day. Then thread the full digest as one reply to
  that message. When the chat tool is unavailable, skip these Slack steps
  and deliver only the run report.

  Do not post the digest to GitHub by default. Only when the team has
  explicitly asked for tracking-issue delivery (and confirmed the digest
  belongs there if acme/widgets is public), use the fallback flow:
  find the open issue titled exactly "Ship digest" with search_issues
  (query `repo:acme/widgets is:issue is:open in:title "Ship digest"`),
  create it with issue_write only if missing, and append the day's digest
  as one comment with add_issue_comment.
mounts:
  - kind: git
    repository: acme/widgets
    mountPath: /workspace/repo
    ref: main
    depth: 300
    auth:
      kind: githubApp
      capabilities:
        contents: read
        pullRequests: read
        issues: write
        checks: read
        actions: read
workingDirectory: /workspace/repo
tools:
  github:
    kind: github
    tools:
      - search_pull_requests
      - pull_request_read
      - actions_list
      - actions_get
      - search_issues
      - issue_write
      - add_issue_comment
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connection
      provider: slack
      connection: slack
      optional: true
triggers:
  - name: digest-heartbeat
    kind: heartbeat
    cron: 0 8 * * *
    timezone: America/Los_Angeles
    routing:
      kind: spawn
  - name: mention
    event: chat.message.mentioned
    connection: slack
    optional: true
    where:
      $.chat.provider: slack
      $.auto.authored: false
    message: |
      {{message.author.userName}} mentioned you on Slack:

      {{message.text}}

      Channel: {{chat.channelId}}
      Thread: {{chat.threadId}}

      Reply in that thread with chat.send. If the user clearly asks for an
      unscheduled digest, produce one. If required context is missing, ask for
      the digest window. Otherwise, briefly explain that you post the daily
      shipped-code digest for acme/widgets in #dev.
    routing:
      kind: spawn
```

Details that matter:

* **`depth: 300` on the mount.** Analyzing a time window needs more git history than a default shallow clone; the prompt still teaches the agent to `git fetch --shallow-since` when the window outruns the checkout.
* **`issues: write` exists only for the opt-in tracking-issue fallback** described in the prompt; the default delivery writes nothing to GitHub. Tighten it to `issues: read` and drop `issue_write`/`add_issue_comment` if you never want that path available.
* **The mention trigger is a second, human entry point** — an unscheduled digest on request. A manual or mention run has no `heartbeat` payload, so the request itself must define the window; the mention message tells the agent to ask for it when it is missing.

## Walkthrough

<Steps>
  <Step title="Apply creates the schedule">
    On apply, auto reconciles one Temporal schedule for the `digest-heartbeat` trigger. Edit the cron or timezone and re-apply, and the schedule updates; delete the trigger and the schedule goes with it.
  </Step>

  <Step title="08:00 America/Los_Angeles: the tick fires">
    The schedule records a heartbeat event carrying `heartbeat.scheduledAt` and routes it. The trigger's `routing: { kind: spawn }` starts a fresh digest session for this tick. Duplicate ticks are impossible: the event's dedup key is the schedule id plus the scheduled timestamp.
  </Step>

  <Step title="The session gathers the window">
    The sandbox boots with `acme/widgets@main` mounted (300 commits deep). The agent computes the 24-hour window ending at `scheduledAt`, then collects merged PRs (`search_pull_requests`), direct-to-main commits (`git log --first-parent` with explicit ISO bounds), per-PR bodies and diffs (`pull_request_read`), and CI runs on main (`actions_list`) to confirm the merges deployed.
  </Step>

  <Step title="The digest lands twice">
    The session's final message is the digest itself — readable later from the project's sessions view. When Slack is connected, the agent also calls `chat.send` once for the one-sentence summary in `#dev`, keeps the returned `threadId`, and threads the full digest as a single reply, so the channel stays scannable.
  </Step>
</Steps>

## Variations

* **Pick your clock.** `cron` accepts standard 5-field expressions (`0 8 * * 1-5` for weekdays) and `timezone` any IANA zone; the string is passed to the underlying scheduler verbatim.
* **Weekly instead of daily.** `cron: 0 8 * * 1` and change the prompt's window arithmetic from 24 hours to 7 days — the window is defined entirely in the prompt.
* **Richer destinations.** Add a hosted connection tool (Notion is a built-in provider) or any remote MCP server and publish the full digest there, letting the Slack message carry a link instead of a thread. See [tools](/reference/tools).
* **Digest more than one repo.** Add a second git mount and extend the prompt's gather list; mounts merge by `mountPath`, so imports and overrides compose cleanly.
* **Smoke test without waiting for the cron.** Start a session directly from the web app with the message "Produce a digest for the last 24 hours ending now." and confirm the report and the Slack thread. Manual runs have no `{{heartbeat.scheduledAt}}`, so the message supplies the window explicitly.
