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, Trigger reads get_daily_activity and list_open_items once, keeps only the configured report connectors with usable coverage, and freezes the remaining governed responses in its active trigger_runs row. The scheduled agent's report-only spine credential can read readiness and that exact run-bound evidence snapshot; it cannot read the live evidence tools or call write tools. Trigger reuses the snapshot on managed-agent attempts and whole-task retries. A retry that adds newly excluded sources fails closed and needs a new run. Trigger validates and hydrates each draft against it. 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 only (Trigger dashboard), mode: "preview"Parses the retired Active Projects workbook (from Drive, via the documents table) and records a workbook-vs-SABLE diff in portfolio_import_runs/portfolio_import_rows. It never writes project records: the apply path was removed on 14 Oct 2026 (the workbook was a one-time baseline and SABLE's records are newer), and any other mode aborts.
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, an ID absent from its frozen snapshot, or a project ID that conflicts with the cited evidence. Check lastGenerationError and evidence_snapshot_id in the failed run's metadata. The frozen snapshot remains in that failed trigger_runs.result for comparison with the managed-session output. It is replaced by the delivery result when a run completes.
  • 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​

  • trigger/reports.ts — board generation, delivery, and the learning loop.
  • trigger/portfolio.ts — daily synthesis and workbook ingestion.
  • trigger/chat.ts, trigger/prompts.ts — the chat agent and its system prompt.
  • trigger/queues.ts — connectorQueue / connectorWriteQueue definitions (consumed elsewhere).
  • trigger/lib/report-contract.ts, trigger/lib/report-edge.ts — report contract types, REPORT_TIMEZONE, REPORT_CONNECTORS, and the spine/managed-agent callers.
  • trigger/watchdog.ts — sendOpsAlert, 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.