Daily briefs
A morning job writes each person's brief from yesterday's meetings and stores that prose.
A daily brief is what happened yesterday at that person's level, written as prose. Nothing is emailed. The brief is stored and read through the API.
The morning job follows docs/decisions/0101-a-brief-is-yesterdays-meetings.md, with the meeting selection in docs/decisions/0114-a-brief-leaves-out-the-recipients-own-meetings.md. The day is still yesterday in America/New_York (docs/decisions/0084-a-brief-is-what-happened-yesterday.md). Who counts as in the room still follows the org chart (0075). For the person named there, that includes the product and engineering organizations: people whose jobs are in that mandate, and the people who report to them. A one-word display name matches a person when that first name belongs to only one person in scope.
The intelligence app shows Today at /intelligence/briefing. The tab is open to [email protected]. An email added on that page is stored in that browser. The Plaud app shows the same brief as Today, and only for [email protected], [email protected], or [email protected]. The morning job stores a draft. Today and the phone keep showing the last published brief until a platform admin publishes that draft at /intelligence/briefs. GET /v1/briefs returns the caller's published brief. It does not return a draft. Reloading that page reads the stored brief and does not write a new revision. Reading every recipient needs foundry::ReadAnyBrief and all=true. Reading the drafts needs the same action and drafts=true.
services/foundry/internal/brief/run.go runs the job. services/foundry/internal/brief/narrative.go chooses the meetings and holds the writing instruction.
The Plaud app shows the section titles as headings. Markdown headings in pasted text are headings too. Listen reads the stored brief aloud in a generated voice, not the phone's built-in voice. The phone asks Foundry for the audio, and Foundry asks the broker.
How a brief is built
- Roster. The recipients come from Intelligence (
GET /v1/people), with each person's title, manager, and direct reports. The call sends the service token andX-Flow-On-Behalf-Of, because Intelligence requires a person on a service call. If Intelligence cannot be reached, the job falls back to a built-in list for AJ Nead, Sara Garner, and Ilan, records a coverage gap, and still includes every address on the default recipient list. - Role. Foundry calls Workspace (
GET /v1/context/person?mode=brief) for the title and the reports. If Workspace is down, the run continues with the role from the roster. The private context is not copied into the brief. - Meetings. Foundry lists yesterday's Zoom transcripts (
GET /v1/me/evidence) and reads the lines of the ones it keeps (GET /v1/meetings/{id}/utterances). A meeting is kept when a direct report or someone in a documented mandate was in the room and the recipient was not. The recipient already knows what was said in their own meetings, so those are left out (0114). A person with no direct reports and no documented mandate is briefed on their peers instead, the people who share their manager: a meeting is kept when a peer was in the room and the recipient was not, and a meeting their manager held without a peer is left out (0117). Rooms with more of those people come first, then longer transcripts. The first meeting of each person in scope is kept, so a longer room does not push out another report's only meeting. A second meeting for someone already covered is left out. At most twelve. A virtual tour is left out. If the list or a chosen transcript cannot be read, the job stores nothing for that person and the pass fails so it can be retried. - Writing. One broker call, with no tools, writes the prose from those transcripts. It is the shortest direct version of each point, in the model's own words: one sentence for each meeting, a second sentence only when a figure or a decision does not fit in the first, and under 350 words in total, not a set of quotes. It does not write a second paragraph or restate the plan. It does not omit a meeting, and it does not drop a report's concrete point to make room for the Waiting or stalled section. It names the person and the action. It does not restate a point, hedge, or set a scene. It names who is doing the work and where it stands, and puts first a decision, a change of plan, a problem, or a risk. Every statement about a meeting comes from the meetings. It leaves out a decision the transcript does not settle, a date nobody stated, pay, equity, a candidate interview with no decision, and treasury movements. It does not give the reader a research task, and it does not recap their own pull requests. The model's text is the brief. A brief written from peers' meetings also leaves out anything about a person's employment, such as hiring, firing, performance, pay, or leave, and discussion of who should take on or stop a piece of work. Their brief also covers other people's pull requests from that day that change files they changed in the last two weeks, read from Azure DevOps. If they opened a pull request in the last two weeks, it also covers pull requests their peers opened or merged that day, and the code work gets about as much space as the meetings. The same brief also names a decision that is waiting or stalled. Those come from the person's context (
GET /v1/context/person?mode=brief) when a thread cites a project record and its stored state iswaitingorblocked, oropenwith a next-step date already past. A meeting line from yesterday is included when it says someone is waiting, blocked, stalled, or cannot proceed until another person acts. A completed action stays in the meeting prose. When there is at least one, the first section is headed Waiting or stalled, before the meeting prose, and each one is one sentence: the decision, who it is waiting on, and why it is stuck when the line says. If there are none, the brief adds no heading and no sentence for them. The text isNothing new.only when no meeting was in scope, no code work was in scope, and no such decision is on record. A person briefed on peers getsNothing new.only when no peer had a meeting without them, nobody else changed their files, and no such decision is on record.
What gets stored
Each run is one revision of one person's brief for one local date. A row records:
- the status:
draft,complete,incomplete,failed,attempt, orsuperseded; - the reason, when the status is not
complete; - the recipient's id, the prompt version, the model, the person-context profile version, the time sources were read up to, and when it was generated;
- coverage: which sources were checked, which meetings were read, the retrieval version, and the selection policy.
draft is the morning job's text before anyone publishes it. It is not what Today or the phone shows. A meeting list or a transcript that cannot be read does not store a row. A pasted or published brief stays in place.
brief_feedback stores a reason from the recipient: wrong_scope, immaterial, already_resolved, missing, ungrounded, or useful. Feedback does not change the brief.
Nothing new. is stored when yesterday's meeting list was read, none of those meetings were in scope, and no stored decision is waiting, blocked, or open past its next step. The brief does not include a diagnostic about what was not checked.
Reruns and regeneration
A rerun reuses today's draft when the prompt version, model, profile version, retrieval version, and selection policy match, and no newer source content has been read. A changed prompt, a changed profile, or newer source content writes a new draft. The older draft is marked superseded and is kept. A published brief is left in place. A new unsuccessful result does not replace a draft or a published brief. It is stored as an attempt.
To force a new revision, call the job with regenerate=true. Add email for one person and date for a past day. Sources are then read up to the end of that day.
Who can read a brief
GET /v1/briefs?date=YYYY-MM-DD returns the caller's own brief. Omit the date to read today in America/New_York. Today is open to [email protected] and [email protected]. Reading another person's published brief needs the foundry::ReadAnyBrief action, which platform admins hold. [email protected] may also read published briefs. Pass recipient=<email> for one person or all=true for everyone. Drafts need foundry::ReadAnyBrief and drafts=true.
POST /v1/briefs stores text a platform admin pasted on Today (/intelligence/briefing). The body is { "data": { "email", "body", "date" } }. date defaults to today in America/New_York. The stored row is complete, and the morning job does not replace it. POST /v1/briefs/{id}/publish publishes one draft. That becomes the complete brief for that person and date, and the previous published row is marked superseded.
POST /v1/briefs/speech returns MP3 for one passage of the caller's own stored brief. The body is { "data": { "text", "date" } }. text has to be part of that stored brief. date defaults to today in America/New_York. The response is audio/mpeg, not the usual envelope. Foundry asks the broker, and the broker uses its OpenAI credential. The model and the voice are fixed.
The morning job
POST /internal/jobs/daily-briefs writes a draft for each recipient and returns when they are stored. It does not publish. If any recipient is not stored as a draft, the response is HTTP 503 so the scheduler retries that pass. An unchanged draft is reused. Kestra calls it at 07:00 America/New_York, including when the clocks change, and recovers a missed morning. There is no later pass the same day.
POST /v1/briefs/feedback records a reason about one stored brief. It does not write a new revision.
services/foundry/.azure/kestra/personal-reports.yaml.
The call uses the broker. The broker needs OPENAI_API_KEY. A missing key fails the job before a brief is stored. Each lookup is made as that person. The writing call has no tools. The job does not send email, text, or announcements. send_email, send_sms, and send_announcement are refused by name.
Locally, the same loop is go run ./services/foundry/cmd/reportrunner with DATABASE_URL set and the broker, intelligence, and workspace processes running. It accepts -email, -date, and -regenerate.
Who gets a brief
Until the shadow stage passes, the audience is AJ Nead, Sara Garner, Ilan, and Adam Neumann. The setting is in the environment, and adding a person does not need a code change.
| Variable | Meaning |
|---|---|
BRIEF_RECIPIENTS | Comma-separated emails. Unset means AJ Nead, Sara Garner, Ilan, and Adam Neumann. all means everyone in the roster with an email. |
BRIEF_RECIPIENT_LIMIT | The most recipients written in one pass. Unset means no limit. |
AZURE_DEVOPS_TOKEN | Reads pull requests, reviews, and comments across the project. Optional. Without it the brief records a gap. |
REPORT_MODEL | The model the broker uses to write. Defaults to gpt-5-mini. |
The rollout is checked against AJ Nead, Sara Garner, Ilan, Adam Neumann, a leasing agent, a property manager, and a worker with little recorded activity before it is widened.