Vocion can mail you now, and the first thing it mails is the daily team report
Release notes 2026-09-15 — outbound mail over Resend and the daily-team-report job (vocion-core#324, merged): what changed, what needs you, are we on track, what happens next — in your inbox, and as a Briefing in the app.

Shipped: vocion-core#324, merged to main on 2026-09-15. Part of the 2026-09-15 release notes.
The outcome
A report nobody opens is not a report. Vocion already published team briefings to /dashboard/briefings every morning; the person they were for was in their email. This release puts the read where the reader is, and orders it the way a manager reads: What changed? What needs me? Are we on track? What happens next? Then the per-team table. Then, last, tokens and cents as evidence.
What it is
Outbound mail. packages/core/src/libs/mail/ — sendMail({ to, subject, html, text }) over a Resend transport using plain fetch. Off by default. The flag pattern matches external workers: VOCION_MAIL_ENABLED=1 turns it on, RESEND_API_KEY and VOCION_MAIL_FROM configure it, and with the flag unset sendMail returns { skipped: true } instead of throwing.
The daily-team-report job — the first built-in do: { job: … } an automation can run (packages/core/src/services/jobs/dailyTeamReport.ts). It folds the trailing window of worker_run rows into team → member stats, counts what is waiting on a human, pulls the latest briefing, renders one HTML mail, stores the same content as a Briefing row so the report is readable in the app even with mail disabled, then sends. Markdown and HTML render from one set of sections (services/reports/renderDailyTeamReport.ts), so the in-app copy and the mail cannot drift.
A follow-up in the same PR lets a workspace mail a team's own briefing in full rather than the workspace rollup: input.briefing: { teamSlug }. The subject becomes that briefing's title.
How a workspace opts in
# automations/daily-team-report.yaml
slug: daily-team-report
name: Daily team report
agent: ceo
when:
schedule: "0 13 * * *" # 13:00 UTC
do:
job: daily-team-report
input:
hours: 24
# to: [ops@example.com] # default: the workspace accountableUser
# automations/morning-briefing-email.yaml — mail one team's briefing
do:
job: daily-team-report
input:
briefing:
teamSlug: revops
Automations are scheduled through Temporal; the job itself is deterministic server code, not an agent turn.
Setup, honestly
Sending needs a domain verified in Resend — DKIM, SPF and (optionally) MX records at your DNS host. Until the domain verifies, the first send fails with a provider error and the Briefing row still lands. The guide walks through it: docs/guides/email.md. A manual trigger exists for testing: npm run report:daily -- --org <id> [--to a@b] [--no-mail] [--html out.html].
For tenants
No behaviour changes until a deployment sets the flag and a workspace ships the automation. With the automation and no flag: one Briefing per fire, no mail.
The screenshot is the Briefings page on a local workspace with fictional data.
Where this lives in the repo
- Transport and flag:
packages/core/src/libs/mail/—mailEnabled()readsVOCION_MAIL_ENABLED === '1';sendMailreturns{ skipped: true }when off;RESEND_API_KEYandVOCION_MAIL_FROMare declared inlibs/Env.tsand documented in.env.example. - The job:
services/jobs/dailyTeamReport.ts, registered inservices/jobs/registry.ts; it is invoked by an automation'sdo: { job: daily-team-report }through the automation runner (a Temporal schedule fires the automation; the job itself is deterministic code). - Collector, sections and rendering:
services/reports/dailyTeamReport.ts,dailyTeamReportShape.ts,renderDailyTeamReport.ts,markdownToEmailHtml.ts; tests alongside. - The stored copy is a
briefingrow (agent_slug = daily-team-report,team_slugNULL for the workspace rollup) and appears on/dashboard/briefings. - Guide:
docs/guides/email.md; the built-in job is listed ondocs/entities/automation.md.
Failure modes: with the flag on and the domain unverified, Resend answers 403 and sendMail raises a MailError with code PROVIDER; the briefing row is written before the send, so the report is never lost. There is no retry queue in this release — the next scheduled fire is the retry.