Skip to main content
Restricted Access: This documentation is only accessible to @tenzo.ai and @salv.ai email addresses.
Status: implemented (2026-09-01). The aggregator endpoint, its generated client, and the job editor’s useOrgTemplateData hook are wired end to end. Editor mount makes one aggregator request for Tier A; the Settings step makes one for Tier B.

Goal

Give the job editor one authenticated read for the org-scoped lists that useOrgTemplateData hydrates on open, instead of a fan-out of per-resource GETs:
No Cosmos schema change. Pure read aggregation over the existing per-resource list handlers. Per-template CRUD stays on the existing feature routers, so mutations and client cache updates keep today’s shapes.

Gate

This endpoint shipped on the editor-composition structural brief, not on measured RUM data. The earlier version of this page gated implementation on a Datadog RUM or Chrome performance trace showing the Tier A / Tier B fan-out was the editor-open bottleneck; that measurement was not taken. Treat the aggregator as a composition simplification whose latency win is expected but unverified until editor-open timings are compared before and after. The Configure-tab document snapshot (jobDocumentStore.lastSaved) is a separate concern. See Job editor document.

Contract

Route

GET /org/{org_id}/editor-bootstrap, served by server/org_templates/editor_bootstrap_api.py and registered under the org-templates tag, so the generated client function lives in the org-templates client module.

Auth

  • Stytch auth is required; the org is resolved from AuthContext.org_id.
  • The path org_id must equal auth.org_id. Any mismatch is a 403 before any read is started. Some of the underlying per-list handlers take only a path org_id without that check; the aggregator does not inherit that gap.

Query

Intended use: tier=a on editor mount; tier=b when the Settings step activates and Tier A is already cached client-side.

Response

EditorBootstrapResponse is flat. It carries the requested tier plus one field per section, and every section reuses the item model of the per-resource list endpoint it mirrors, so the frontend maps the same fields it maps today. There is no second template schema. The recruiting email and the org users map are not sections and stay on their own requests: the recruiting email is a scalar whose legitimate null was indistinguishable from a failed section, and the users map is an app-wide SWR key shared with non-editor pages. A section is null in exactly two cases: the requested tier does not include it, or its read failed. A section that was read and found empty is an empty list, so the client can tell “not fetched” from “fetched empty”. A client that requested a section and received null should treat that section as unavailable and may fall back to the per-list endpoint for it.

Section isolation

Sections are read concurrently and each read is isolated: one section’s failure is logged and returned as null while the other sections still populate. Each section also runs under its own timeout (SECTION_TIMEOUT_SECONDS, 10 seconds), so a section that is still running at that point is returned as null instead of holding the others. The response is 200 whenever the caller is allowed to read the organization, and one info log line records the org, tier, elapsed time and how many of the requested sections populated.

Domain users are deliberately left out

useOrgTemplateData also loads domain users (GET /org/{org_id}/get-domain-users) when a calendar is connected. That handler makes a live Google or Microsoft directory call, and its frontend gate (calendarConnected) resolves after the editor mounts, so folding it into a mount-time read would either block Tier A on an external directory round trip or fire it for orgs that will never use it. It stays a separate SWR key.

Composition

  • The handler is orchestration only. Each section awaits the existing per-resource route handler with an explicit auth context and org id, so default-template seeding, sort order and org re-filters are inherited from those handlers rather than re-implemented.
  • Which sections a tier populates is a pure function of the tier, kept separate from the I/O handler.
  • No new SQL tables, no new DAO methods.

Frontend

useOrgTemplateData keeps every per-list SWR key as the read path and makeListSetter as the write path, so template create, edit, and delete stay local-first and the admin and create-page consumers of this module see no change. The aggregator only changes how each reader inside the hook obtains its list:
  • One aggregator request per tier: the Tier A readers share one tier=a request on editor mount, and the Tier B readers share one tier=b request when the Settings step activates. The frontend never asks for both tiers in one request.
  • The per-list SWR keys stay the read path; each reader takes its own section from the shared response.
  • Per-reader fallback: when a reader’s section is null, or the aggregator request itself failed, that reader fetches its own per-list endpoint. An error toast now means both the aggregator section and the per-list endpoint failed for that list.
  • enableSettingsTier() remains the Tier B gate, so enabling the Settings step does not refetch Tier A.
  • Domain users keep their own request, as described above.
The request sharing and fallback mechanism lives in ui/src/pages/jobs/editor/services/editorBootstrapLoader.ts.

Adjacent Settings calls (not folded in)

These fire from CallerSettingsStep around the same time as Tier B but are not in useOrgTemplateData. They remain separate SWR keys unless a metrics case appears:
  • POST /organization (org job-board email / timezone)
  • POST /organization/brands
  • GET /org/{orgId}/whatsapp-senders/has-approved
Out of scope for this aggregator: job-scoped script fetch, ATS job schema, organization variable names.

Non-goals

  • Renaming backend “campaign” to “job”
  • Changing Cosmos document shapes
  • Aggregating job or script payloads
  • Auto-merge of adjacent Settings calls without a metrics case