Skip to main content

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:

  1. Refresh sources. refreshReportSources re-invokes the edge functions listed in REQUIRED_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.
  2. Check readiness. It calls the spine tool get_report_readiness. The spine's answer is canonical; if it disagrees with the REPORT_REQUIRED_CONNECTORS env 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 returns outcome: "blocked" — no board goes out.
  3. Generate. If ready, it calls the managed report-generation agent (invokeManagedReport), up to REPORT_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.
  4. Persist and deliver. The draft is submitted (submit_board_draft), then handed to deliverPersistedReport, a separate idempotent task on reportDeliveryQueue, via triggerAndWait with 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

TaskTrigger / cronWhat it does
morningBoardschedule, cron pattern 0 6 * * 1-5 (weekdays only), timezone set from the REPORT_TIMEZONE constant in trigger/lib/report-contract.tsRefreshes required connectors, checks readiness, generates and delivers the morning board.
eodPacketschedule, cron pattern 30 16 * * 1-5 (weekdays only), timezone REPORT_TIMEZONETriggers dailyPortfolioSynthesis, then runs the same refresh/readiness/generate/deliver pipeline as morningBoard for the "eod" kind.
learningLoopschedule, cron pattern 0 17 * * 1-5 (weekdays only), timezone REPORT_TIMEZONEInvokes the managed learning-loop agent for the report date; logs an incident on failure.
deliverPersistedReportevent — triggered by runScheduledReport via triggerAndWaitIdempotent 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 dateScans 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() promptThe 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_runs row for sable.report.morning / sable.report.eod and its outcome field: "blocked" means readiness failed (check stale_sources / missing_sources in the run output); the independent watchdog described in watchdogs.md is 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_CONNECTORS disagree, 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 / assertExcludedSourcesAbsent on both attempts; check lastGenerationError in the failed run's metadata.
  • A delivery failure with an unhelpful message. A failed triggerAndWait carries error: unknown, not a real Error — this is why taskRunErrorMessage exists 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 approvals row with code = '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. ingestPortfolioBaseline in "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.tsconnectorQueue / connectorWriteQueue definitions (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.tssendOpsAlert, imported here for best-effort ops alerts (see watchdogs.md for the independent verification side).
  • Supabase: board_runs (migration 0027_board_runs.sql) is the delivered record the watchdog reads; trigger_runs (0041_trigger_runs.sql) is the per-invocation audit trail; audit holds recorded incidents (sable_report_incident); portfolio_import_runs / portfolio_import_rows (migration 20260722204531_portfolio_operating_system.sql) back the workbook import; projects, project_operating_profiles, approvals, sources, tasks, and promises are the evidence tables dailyPortfolioSynthesis reads from.