Reports and Boards
Overview
trigger/reports.ts generates and delivers SABLE's morning and end-of-day
boards, plus a daily learning-loop pass over that same reporting data.
trigger/portfolio.ts's dailyPortfolioSynthesis — triggered by the EOD
board rather than its own schedule — proposes daily project-status updates
from the same evidence window. trigger/chat.ts and trigger/prompts.ts are
grouped here too: sableChat is the interactive surface for asking questions
about this same board and portfolio data, and it has no cron of its own, so
it fits alongside the reporting pipeline rather than in connectors,
documents, or watchdogs. trigger/queues.ts is included on this page per the
source layout, even though the two queues it defines (connectorQueue,
connectorWriteQueue) are actually consumed by the connector and document
tasks on the other pages — see the note below.
How it works
runScheduledReport(kind, ...) is the shared engine behind both
morningBoard and eodPacket:
- Refresh sources.
refreshReportSourcesre-invokes the edge functions listed inREQUIRED_REFRESHES(Fathom, Granola, Asana, email, calendar — Drive and Toggl were removed from this list on 2026-09-03 and are no longer allowed to be cited by a board) so the readiness check below reflects this run's own refresh, not stale pre-refresh state. - Check readiness. It calls the spine tool
get_report_readiness. The spine's answer is canonical; if it disagrees with theREPORT_REQUIRED_CONNECTORSenv var, the run logs a drift alert but keeps going with the spine's list rather than failing over a config mismatch. If the spine says not ready, the run records an incident, alerts ops, and returnsoutcome: "blocked"— no board goes out. - Generate. If ready, it calls the managed report-generation agent
(
invokeManagedReport), up toREPORT_GENERATION_ATTEMPTS(2) times, validating and hydrating the draft against canonical evidence (get_daily_activity,list_open_items) each attempt. A model output that cites an excluded source, or otherwise fails the deterministic contract, is a rejected attempt, not an automatic run failure. - Persist and deliver. The draft is submitted (
submit_board_draft), then handed todeliverPersistedReport, a separate idempotent task onreportDeliveryQueue, viatriggerAndWaitwith its own idempotency key.
eodPacket does one extra thing before generating its own board: it triggers
dailyPortfolioSynthesis (idempotency-keyed per day) so the daily portfolio
proposal runs alongside, not instead of, the EOD report.
learningLoop is simpler — it resolves the org, opens a trigger_runs row,
and calls the managed learning-loop agent for the day; failures are recorded
the same way report failures are.
Tasks
| Task | Trigger / cron | What it does |
|---|---|---|
morningBoard | schedule, cron pattern 0 6 * * 1-5 (weekdays only), timezone set from the REPORT_TIMEZONE constant in trigger/lib/report-contract.ts | Refreshes required connectors, checks readiness, generates and delivers the morning board. |
eodPacket | schedule, cron pattern 30 16 * * 1-5 (weekdays only), timezone REPORT_TIMEZONE | Triggers dailyPortfolioSynthesis, then runs the same refresh/readiness/generate/deliver pipeline as morningBoard for the "eod" kind. |
learningLoop | schedule, cron pattern 0 17 * * 1-5 (weekdays only), timezone REPORT_TIMEZONE | Invokes the managed learning-loop agent for the report date; logs an incident on failure. |
deliverPersistedReport | event — triggered by runScheduledReport via triggerAndWait | Idempotent delivery of an already-persisted board draft (deliver_board spine tool), honoring shadow vs. live delivery mode. Runs on reportDeliveryQueue. |
dailyPortfolioSynthesis (portfolio.ts) | event — triggered daily from inside eodPacket, idempotency-keyed per report date | Scans active/watch/deal-pending projects, looks at the last 7 days of sources/tasks/promises, refreshes project_operating_profiles, and proposes a portfolio_change approval per project with new evidence. |
ingestPortfolioBaseline (portfolio.ts) | event — manual/UI-triggered, mode: "preview" or "apply" | Parses the canonical Active Projects workbook (from Drive, via the documents table) and either previews or applies the resulting project/client/owner mapping through portfolio_import_rows and an RPC batch apply. |
sableChat (chat.ts) | event — one invocation per chat session (chat.agent) | The durable SABLE chat agent: resolves sableChatSystemPrompt at session start, then streams a Claude response (claude-sonnet-5 via the AI SDK) bounded to 8 steps. |
sableChatSystemPrompt (prompts.ts) | not a task — a versioned prompts.define() prompt | The system prompt sableChat resolves each session; parameterized by org name, viewer name/role, and today's date. |
Assignment note
chat.ts and prompts.ts have no cron and don't sync or clean up anything on
their own — they're the conversational read path over the same board and
portfolio data the rest of this page produces, so they're documented here
rather than split into their own page. queues.ts is likewise not a
board-specific file on its own: connectorQueue and connectorWriteQueue are
defined there but are actually consumed by the connector tasks
(connectors.md) and the document-indexing tasks
(documents.md). The queues used by this page —
reportQueue, reportDeliveryQueue, learningQueue (reports.ts), and
portfolioQueue (portfolio.ts) — are each defined locally in this page's
own source files, all with concurrencyLimit: 1.
What can go wrong / how to investigate
- A board silently didn't go out. Look at the
trigger_runsrow forsable.report.morning/sable.report.eodand itsoutcomefield:"blocked"means readiness failed (checkstale_sources/missing_sourcesin the run output); the independent watchdog described inwatchdogs.mdis the backstop that catches this even if the alert path itself is broken. - Connector-contract drift warning. If the spine's required-source list
and
REPORT_REQUIRED_CONNECTORSdisagree, the run logs a warning and proceeds on the spine's list — update the env var to match rather than treating the warning itself as the bug. - "managed report failed deterministic validation after 2 attempts." The
managed agent's draft cited an excluded source or otherwise failed
parseReportDraft/assertExcludedSourcesAbsenton both attempts; checklastGenerationErrorin the failed run's metadata. - A delivery failure with an unhelpful message. A failed
triggerAndWaitcarrieserror: unknown, not a realError— this is whytaskRunErrorMessageexists to extract the real reason (e.g. a Resend domain-verification failure, the actual cause of the 2026-09-03 delivery incident) instead of stringifying to"[object Object]". - A daily portfolio proposal didn't appear for a project with new
activity. Check whether an
approvalsrow withcode = 'daily_status:<date>'already exists for that project — synthesis is idempotent per project per day and won't propose twice. - Portfolio workbook apply is rejected.
ingestPortfolioBaselinein"apply"mode requires a matching, still-valid"preview"run (previewRunId) whose project snapshot hash hasn't changed since — a stale preview or projects that changed after preview will block the apply with an explicit error rather than applying against outdated data.
Where the code lives
sable-agents-demo/trigger/reports.ts— board generation, delivery, and the learning loop.sable-agents-demo/trigger/portfolio.ts— daily synthesis and workbook ingestion.sable-agents-demo/trigger/chat.ts,sable-agents-demo/trigger/prompts.ts— the chat agent and its system prompt.sable-agents-demo/trigger/queues.ts—connectorQueue/connectorWriteQueuedefinitions (consumed elsewhere).sable-agents-demo/trigger/lib/report-contract.ts,sable-agents-demo/trigger/lib/report-edge.ts— report contract types,REPORT_TIMEZONE,REPORT_CONNECTORS, and the spine/managed-agent callers.sable-agents-demo/trigger/watchdog.ts—sendOpsAlert, imported here for best-effort ops alerts (seewatchdogs.mdfor the independent verification side).- Supabase:
board_runs(migration0027_board_runs.sql) is the delivered record the watchdog reads;trigger_runs(0041_trigger_runs.sql) is the per-invocation audit trail;auditholds recorded incidents (sable_report_incident);portfolio_import_runs/portfolio_import_rows(migration20260722204531_portfolio_operating_system.sql) back the workbook import;projects,project_operating_profiles,approvals,sources,tasks, andpromisesare the evidence tablesdailyPortfolioSynthesisreads from.