Operations

The morning brief

What it reads, what it writes, and how to customize it.

Updated · 2026-05-28

The morning brief is Mayva’s flagship worker. It runs at 06:00 local, reads your context, and writes a structured plan for the day.

What it reads

In order of priority:

  1. Last night’s recovery (health_signals kind=recovery) — the band drives the tone.
  2. Sleep duration & efficiency (health_signals kind=sleep).
  3. Today’s calendar (calendar_events where date = today).
  4. Yesterday’s outcomes (outcomes joined to tasks from yesterday).
  5. Active goals (data/goals/*.md).
  6. Profile preferences (data/profile/preferences.md, prohibitions.md).
  7. Inbox signal (email_signals unread count from non-newsletter senders).

All of this is composed in packages/core/agent/context.ts::buildBriefContext(). About 1,500 tokens of input on a typical day.

What it writes

const MorningBriefSchema = z.object({
  date: z.string(),                              // YYYY-MM-DD
  recoveryBand: z.enum(['green','yellow','red','none']),
  body: z.string(),                              // 3-5 sentence narrative
  items: z.array(z.object({
    title: z.string(),
    priority: z.enum(['p0','p1','p2']),
    durationMin: z.number().int().positive(),
    suggestedStart: z.string().optional(),       // ISO timestamp
    reason: z.string(),                          // why this item, not just what
  })),
  protectives: z.array(z.string()),              // "skip heavy lifting today"
});

One row in morning_brief per date. Re-running overwrites idempotently.

How tone changes with recovery

packages/core/agent/morning-brief.ts swaps a fragment of the system prompt by band:

BandTone fragment
green”It’s a green day. Schedule the hardest work first. Don’t waste it.”
yellow”Recovery is yellow. Protect deep work. Defer hard meetings. Two items on the must-do list, not five.”
red”Recovery is red. This is a protective day. Default to lower-strain work. Re-evaluate plans that require physical or social energy.”
none(no fragment — pure planning, no health editorializing)

These fragments are strings in the file. Edit them. Re-run pnpm brief. Done.

Customizing the prompt

// packages/core/agent/morning-brief.ts
const SYSTEM = `
You are Mayva's morning brief author. Your job is to plan the day for the user
based on the context provided. Be concise. Lead with the recovery state.

${toneFragment(band)}

Output structured JSON matching MorningBriefSchema. The body field is 3-5
sentences total, narrative voice, second-person. The items field is 2-7 entries.
Priorities: p0 is non-negotiable (health, safety, scheduled commitments), p1 is
work toward active goals, p2 is everything else.

Never invent calendar events. Never say "I" — the brief is your output, not your
voice. Never editorialize about health beyond the tone fragment above.
`;

Common edits:

  • Switch model: change model: 'claude-3-5-sonnet' to 'claude-3-5-haiku' for ~4× cheaper at modest quality loss.
  • Different time: change cron in packages/workers/src/scheduled/morning-brief.ts. The cron string is on line 5.
  • Different schema: edit MorningBriefSchema in packages/shared/src/schemas/brief.ts. The dashboard’s MorningBriefCard reads the same schema, so it’ll keep working as long as the shape stays compatible.
  • Different memory layers: add to buildBriefContext() in packages/core/agent/context.ts.

When the brief decides not to run

If recovery for today is missing and Whoop is enabled, the brief waits up to 30 minutes for the next whoop-sync to populate it. After that, it runs with band: 'none'.

If data/profile/ is empty (fresh install), the brief refuses to run and emits a single suggestion: “set up your profile — see docs/getting-started.” Defensive — we don’t want a useless brief on day one.

Reading yesterday’s outcomes

A reliable brief considers what you actually did, not just what you planned. The outcomes table holds explicit completions, but most “done” signals are implicit (the task is past its window, it’s not marked deferred, no further activity). The brief composer uses a getYesterdayOutcomes() function that:

  1. Lists tasks with status='done' from yesterday.
  2. Lists tasks with status='pending' from yesterday → likely missed.
  3. Looks at worker_runs for any agent-dispatched actions that completed.

This gives the brief enough to say “you missed the finance review but knocked out the hard work in the morning; today, protect the morning again.”