Skip to main content

Portfolio API

Six routes back the Portfolio surfaces: portfolio, portfolio-health, portfolio-import, portfolio-options, portfolio-actions.

GET /api/portfolio

Scoped account/project payload — buildPortfolioPayload, scoped through resolvePortfolioScope/scopeRequest/visibleProjectIds, the same scope helpers /api/chat and /api/spine build on. publicViewer produces the narrower shape a client-role viewer sees.

GET /api/portfolio-health

Admin/lead, read-only. Board coverage and open-item verification, computed on read rather than stored — this is deliberately the home for portfolio-level data-quality findings that used to arrive as board_update approvals with no project attached, which meant they were approvable but could never actually write anything. VERIFY_WINDOW_DAYS (14) matches STALE_AFTER_DAYS in _portfolio_health.js, and the staleness count is computed server-side in SQL rather than pulling thousands of rows into JavaScript to count client-side.

GET / POST /api/portfolio-import

{ "mode": "dryRun" }

GET lists the last 20 import runs (portfolio_import_runs) for the org, with parser version, counts, validation, and any error. POST starts an import — either inline or, when Trigger is configured, enqueued via triggerTask rather than run synchronously in the request.

GET /api/portfolio-options

Admin/lead. Returns clients and people lookups for populating portfolio dropdowns/forms — a small convenience route so the frontend doesn't have to duplicate these queries.

POST /api/portfolio-actions

A dispatcher for several distinct mutations, each normalized and validated before it reaches the database: status updates (appendStatus), account creation (normalizeAccountCreation), client create/update (normalizeClientCreation/normalizeClientUpdate), operating-profile updates, portfolio classification, reconciliation resolution, and project updates. Each mutation type has its own normalizer specifically so a malformed request fails with a clear statusCode and message before it ever reaches a database write — throwPortfolioMutationError turns any mapped error into exactly that shape.

Where the code lives

  • sable-agents-demo/web/api/portfolio.js, portfolio-health.js, portfolio-import.js, portfolio-options.js, portfolio-actions.js
  • sable-agents-demo/web/api/_portfolio.js, _portfolio_health.js, _portfolio_scope.js, _portfolio_mutation.js, _account_creation.js, _client_mutation.js, _project_update.js
  • sable-agents-demo/trigger/portfolio.tssable.portfolio.ingest-baseline, sable.portfolio.daily