Skip to main content
A senior chief of staff doesn’t just say “this needs attention”; they remember what they said yesterday and ask whether it moved. Phase 3B adds that memory.

What changed

Each Daily Brief now ends with 1-3 specific actions, queued for you to mark taken or dismissed. The next morning’s brief receives that history and can reference it directly:
“Monday’s coaching for Sarah hasn’t moved her CSAT yet; second pass needed. The flat AI deflection still tracks to last week’s call to widen the FAQ — push that this week.”
If you took an action and the metric moved, the brief says so. If you dismissed it and the underlying issue is still live, the brief surfaces it again with that context. If it’s still pending after a few days, the brief notes the staleness rather than silently re-recommending.

Where actions show up

  • In-app Daily Brief — the prose ends, then a small numbered list with a checkbox per action. Click to mark taken; “Dismiss” link to clear it from the queue without claiming it was done.
  • Email Daily Brief — actions render as a “Today’s actions” block beneath the prose. The marking happens in the app (email links open the app); the email is the notification, not the interaction surface.
  • Slack Daily Brief — same pattern: a “Today’s actions” section under the brief body with a one-click link back to the app to mark them.
The Weekly Watch digest is currently action-free. Action follow-through is a daily-cadence affordance; pushing it weekly dilutes the loop.

Action shapes

The model is instructed to emit actions that are:
  • Verb-first: “Coach Sarah on response time”, “Investigate Tuesday’s CSAT dip”, “Push back on the new SLA targets”.
  • Concrete: actionable by end-of-day, not vague directional advice.
  • Under 90 characters: one line, no rationale (the rationale lives in the prose above).
If the data doesn’t justify any specific action (“not enough data yet” briefs), no actions are emitted.

Statuses

Each action carries one of three states:
  • Pending — default. Sits in the queue.
  • Taken — you acted on it. Tomorrow’s brief receives this and references the result if visible in the metrics.
  • Dismissed — not the right call. Tomorrow’s brief sees the dismissal and shouldn’t re-recommend the same thing without different framing.
The history window is 7 days. Older actions remain in the database (dismissals and takes are permanent) but stop appearing in the prompt context — the brief shouldn’t be relitigating last month.

What the next brief sees

The buildPrompt() function injects an ACTION HISTORY block listing each recent action with its age, status, and (where available) the measured 7-day outcome:
ACTION HISTORY (your prior briefs, last 7 days):
• Coach Sarah on response time (queued yesterday, marked taken)
• Investigate Tuesday's CSAT dip (queued 2d ago, still pending)
• Push back on the new SLA targets (queued 3d ago, dismissed)
• Coach Tom this week — CSAT falling (queued 8d ago, marked taken) → +4pp csat in 7d (worked)
• Audit backlog this week (queued 9d ago, marked taken) → -1d backlogAge in 7d (no movement)
The model is told to reference these where relevant and not to fabricate progress on actions it wasn’t told moved. If it re-recommends a still-pending action, it must say so plainly (“Still the right call from Tuesday: …”) rather than presenting it as a fresh idea. When an outcome is attached, the brief references the real movement by number (“Last week’s coaching lifted Sarah’s CSAT by 4pp.”).

Outcome attribution

Each action created from a Daily Brief — and each auto-flag action Forepost queues itself — is stamped at creation with two extra fields:
  • metric_kind: which metric the action is most likely to move (csat / volume / firstResponse / deflection / utilisation / backlogAge / oneTouch). Inferred from the action text by a keyword match (“coach” → csat; “hire” → utilisation; “deflect” → deflection; “backlog” → backlogAge; etc.). When nothing matches, the field stays null and the action is excluded from attribution.
  • metric_baseline: the workspace value of that metric at the moment the action was created.
Seven days after creation, a nightly attribution job reads the current workspace value and computes the delta vs. baseline, writing it to metric_delta_7d with an attributed_at timestamp. The brief’s history block then shows the outcome inline. Honest semantics:
  • Deltas below ±0.5 are tagged no movement — sub-resolution drift is noise, not a result.
  • For metrics where higher is better (CSAT, deflection, one-touch), a positive delta is worked.
  • For metrics where lower is better (FRT, backlog age, utilisation, volume), a negative delta is worked.
  • Anything else is did not work.
The attribution job runs every cron tick, capped at 100 actions per pass. Actions older than 14 days are skipped — the user has moved on; attribution at that point is ambient noise, not a useful signal. Admins can inspect aggregate outcome rates at GET /admin/attribution, which returns per-metric “worked / neutral / regressed” counts and average deltas.

Why we don’t store rationale

Actions are deliberately just text. We considered storing the model’s reason for each action and surfacing it in tomorrow’s history, but that doubled the prompt budget and added a layer of indirection that the leader doesn’t actually need. The prose brief is the rationale. The action queue is the pointer back to it. If you need the original context for a stale action, the brief that produced it is in the Archive.

Draft and dispatch

Each pending action now carries three extra affordances in-app: Draft Slack, Draft email, and → Linear.
  • Draft Slack / Draft email — generate a copy-ready message the leader can send. One Haiku call per request; the draft is written in the leader’s voice (confident, direct, no exclamation marks, no greetings on Slack), references real numbers from the workspace, and ends with a specific ask. Drafts are saved in action_drafts so coming back to the page shows the last generation without re-spending the call. Rate-limited at 30 drafts per hour per workspace.
  • → Linear — files the action as a Linear issue in the workspace’s connected Linear org. Title is the action text (truncated to 80 chars); description is the full text plus a footer link back to Forepost. Requires Linear to be connected in Settings → Integrations → Linear (Linear integration). Returns the issue URL on success.
These affordances only render on pending actions — once an action is taken or dismissed, the row collapses to its status, and re-running a draft for a closed action would be confusing.

API

For workspace owners using the public API:
EndpointMethodPurpose
/actionsGETList actions for the authenticated user (default: last 7 days). Query params: since (ms epoch), limit (max 200).
/actionsPOSTCreate one or more pending actions. Body: { actions: [string], briefId?: number }. Used by the SPA after parsing a brief; not needed for typical integration.
/actions/:idPATCHUpdate an action’s status. Body: { status: "pending" | "taken" | "dismissed" }.
/actions/:id/draftPOSTGenerate a Slack-DM or email draft. Body: { channel: "slack" | "email", recipient?: string }. Returns { draft: { id, channel, text, createdAt } }.
/actions/:id/linearPOSTFile the action as a Linear issue. Optional body: { teamId: string } to pick a Linear team explicitly; otherwise the first accessible team is used. Returns { ok, issue: { id, identifier, url } }.
All endpoints require Clerk auth.

Limits

  • Actions per brief: capped at 5 (the parser drops extras).
  • Action text length: 240 chars max (truncated server-side).
  • Action writes per user: 60/hour.
These are deliberately loose; the model rarely produces more than 3 and the user-facing UI caps interaction at the same level the prompt does.