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 thatuseOrgTemplateData hydrates on open, instead of a fan-out of per-resource GETs:
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_idmust equalauth.org_id. Any mismatch is a 403 before any read is started. Some of the underlying per-list handlers take only a pathorg_idwithout 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 asnull 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=arequest on editor mount, and the Tier B readers share onetier=brequest 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.
ui/src/pages/jobs/editor/services/editorBootstrapLoader.ts.
Adjacent Settings calls (not folded in)
These fire fromCallerSettingsStep 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/brandsGET /org/{orgId}/whatsapp-senders/has-approved
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