Getting the data out of BrokerageBuilder

What is actually in there, how it maps to V1, and why we send an agent instead of building a connector

Overview Engineering BKB Extraction Executive Client-facing

Baldwin confirmed on the Aug 11 discovery call that they run Zywave BrokerageBuilder, that it is meant to be their system of record including documents, and that ThreeFlow was given direct back-end access and extracted the data themselves. That precedent is the whole ballgame: the access model we need is one a customer has already granted a vendor once.

Sourced to the call, not to memory

Laura Wieners at 24:49: "They were able to directly go into Zywave and BKB and extract it out themselves. They had access to that behind the scenes." On documents, at 25:45: "They're supposed to be there," and Tina Lindsay: "It is supposed to be our system of record, our source of truth for everything." Steve asked for API privileges and tokens at 45:22; Laura agreed to pursue it.

⚠️ Note the hedge in "supposed to be." Capability is confirmed. Actual completeness is not, and the internal read is that BKB is used mainly for commissions with thin client records. Plan for a partial source.

The decision: an agent, not a connector

Steve's call. The API spec is the map, not the build target.

We are not writing a typed BrokerageBuilder client. We are building an extraction agent that is handed credentials and goes and gets the book. Five reasons, and the first one is decisive:

WhyWhat breaks a hard-coded connector
Their taxonomy is tenant data, not enums Plan types, benefit items, and tier types are all configured per instance and served from /plans/plantypes, /plans/plantypes/{id}/benefititems, and the tier-type endpoints. Benefit values are free-text strings. A typed client needs bespoke mapping code per tenant. An agent reads the taxonomy at runtime and maps against it.
Baldwin has multiple BKB instances Tina confirmed CAC and recent partners are consolidating onto the Baldwin instance while some older acquisitions still sit on their own. That is the per-tenant mapping problem multiplied, before we even start.
The data is a partial source Adoption is incomplete, the West historically used shared drives, one region ran on Excel. An agent can flag a gap and route it to review. A connector throws or silently writes a null.
Access may be credentials, not an app registration The documented OAuth path requires the customer to register an app in their own admin. ThreeFlow did not appear to need that ceremony. An agent that can drive either an API token or an authenticated session is not blocked on which one we get.
It generalises The next AMS is a new prompt and a new credential, not a new integration project.

Be honest about the shape: this is a hybrid, not a free-roaming agent

Do not let an LLM do the paging. A deterministic runner handles enumeration, pagination, download, retry, checkpointing, and rate limiting. The agent is called where judgment is actually required: mapping this tenant's plan types and benefit items onto our schema, resolving carrier names, deciding what a document is, and flagging anomalies.

At Baldwin's scale that distinction is not academic. Roughly 4,500 to 5,000 employers, several plans each, and every plan carrying benefit values and rates. A loop that is deterministic where it can be and intelligent where it must be is the only version that finishes.

What BrokerageBuilder actually holds

Parsed from the live OpenAPI spec: 53 paths, 225 schemas.

Employer level

POST /accounts/search then GET /accounts/{accountId}. Name, full address, phone, fax, URL, SIC code, employee and retiree counts, year founded, county, business line, client code, producer, status, plus tenant-defined custom fields.

Plan level

POST /plans/search then GET /plans/{planId}. Plan name, account, issuing and billing carrier, policy number, plan year start and end, plan type, subscriber count, rate type, renewal lineage, notes.

Plan attributes

GET /plans/{planId}/benefititemvalues returns BenefitItemValue rows: benefit item id, tier id, value type, and a free-text value string. The item definitions come from the tenant's own plan-type taxonomy.

Rates

GET /plans/{planId}/rates, plus fees and self-funded rate metadata. Six fixed rate shapes. TieredRate is tier type, count, monthly fee.

Documents

GET /folder for the folder tree, POST /file to search, and GET /file/download for the bytes. Name, MIME type, size, folder, dates, description.

Contacts and carriers

POST /accountcontacts/search for employer contacts, POST /carriers/search plus carrier codes for the carrier crosswalk.

🔴 The documents finding, and it changes the design

There is no plan-to-document relationship in BrokerageBuilder.

Files in BKB hang off an entity, and the EntityType enum has exactly three values: General, Account, Carrier. There is no Plan. SearchFilesCriteria requires entityType and entityId, and folders are scoped the same way.

What this means

We can ask BKB "give me every document for this employer." We cannot ask it "give me the certificate for this dental plan." The link between a document and the plan it describes does not exist in their data model. Folder names and file names are the only structural hints, and at Baldwin those conventions vary by region and instance.

So we do not get that link from BKB. We produce it by reading the document. That is precisely what Open Plan's /docid already does: identify carrier, employer, products, and page ranges from the file itself. The join is an output of extraction, not an input to it.

This is a good outcome, not a setback. It means the document pipeline we already planned is unchanged by whatever BKB does or does not know, and it means the same pipeline works for the regions that never adopted BKB and kept their plans on a shared drive.

Map: BKB employer to V1 employer

V1's Group is DynamoDB-backed and schemaless; the field list below is its own $formLabels enumeration.

BKB AccountInfoV1 GroupNote
namename (Employer Name)Direct
address1, address2, address3address1, address2⚠️ BKB has three lines, V1 has two. Concatenate 2 and 3.
city, stateProvince, zipPostalCodecity, state, zipDirect
countycounty-fips⚠️ BKB gives a county name; V1 wants county plus FIPS. Needs a lookup, and split-ZIP care.
phonephoneDirect. fax has no V1 home.
urlwebsite / authorizedDomainDirect
sicCodesicCodeDirect, but Baldwin flagged this as commonly missing
numberOfEmployeesemployeeCount (Est. Employee Lives)Also derive marketSize from it
clientCodeintId (Reference ID/Code)Best match in the set. Both are "this record's id in another system." Also store the BKB accountId for re-runs.
producerIdprimarySalesLead (Producer)Needs a BKB-user to V1-user crosswalk
accountStatus, brokerageBuilderStatus, isBenefitClient, isBenefitProspectclientStatusCollapse four signals into one enum. Decide the rule once.
businessLine, benchmarkIndustry, classificationprimaryIndustryLoose. Agent judgment, low confidence, review.
customFieldsvariesInspect these first at each instance. Tenant-defined, and in a shop that uses BKB mainly for commissions this is where anything non-standard actually lives.
numberOfRetirees, yearFounded, countrynoneDrop

V1 employer fields BKB cannot fill

V1 fieldWhere it has to come from
taxId (Federal Tax ID)⚠️ Not in BKB's account model at all, and Baldwin already flagged it as spotty on their side. Expect to source it from documents or leave it blank.
officeId🔴 Critical for Baldwin. Four benefits regions with offices under them, and the whole rollout is organised this way. Derive from the BKB instance plus producer, and confirm on a review screen. Getting this wrong misfiles the entire book.
primaryRenewalMonthDerive from the plans: the modal planYearStartDate month across the employer's plans.
acaRatingAreaDerive from ZIP. Watch split ZIPs.
contact, contactTitle, contactPhone, contactEmailPOST /accountcontacts/search, then pick a primary. Rule needs deciding.
naicsCode, corpStructure, companyType, dbaListNo source. Leave empty.
benAdmin, benManagement, benClientIdNo source. Baldwin uses Employee Navigator, so this may be fillable later from that side.
borDate, logo, aboutEmployer, marketing*No source. Not needed for conversion.

Map: BKB plan to V1 plan

Scope is the three questions: who is the carrier, what are the rates, what are the plan attributes.

BKB PlanInfoV1 QuoteNote
planNamename (Plan Name)Direct
accountIdgroupId (Employer)Via the account crosswalk
issuingCarrierIdcarrierId (Carrier)Needs a carrier crosswalk. Resolve through /carriers/search and carrier codes, fuzzy match to V1's carrier list, A.M. Best cross-check, review screen.
billingCarrierIdmguCarrierId (Carrier Paper)Only when it differs from the issuing carrier
carrierPolicyNumberinForcePolicyNumber (Policy / Group #)Direct
planYearStartDateinForceStartDate (Effective Date)Direct
planYearEndDateinForceEndDate (Renewal Date)Direct. Set useDateRange accordingly.
planTypeIdPlanType.name / amsCodeinsuranceType (Plan Type)The per-tenant mapping table. Anchor on basePlanTypeId plus amsCode. Target V1's 36-type registry in config/plantypes.php.
rateTypeplanType (Funding Type) + rateTierSchedule⚠️ rateTierSchedule (string) and rateTiers (int) must both be set
currentSubscribersenrollmentUseful sanity check against the census
planCodeintIdPlus store BKB planId for re-runs
notesnotesDirect
renewedFromPlanIdnoneDo not drop this. It is the renewal chain, which is how you reconstruct prior plan years and pick the current one when an employer has several.
expectedPremium, expectedCommission, commission*, payModeId, billingModeId, departmentIdnoneCommission operations. Out of scope.

Attributes and rates

Attributes: BenefitItemValue.value is a free-text string against a tenant-defined benefit item. The agent maps item names to V1 field keys (enumerated only via Plantypes::tableColumns(), app/Helpers/Plantypes.php:1483) and normalises values into V1's typed token sets. Where a document also exists for the same plan, cross-validate and flag disagreements rather than silently preferring one.

Rates: six fixed shapes into V1's flat rate columns. Fixed, tiered, and volume are clean; age-banded needs parsing. Write raw carrier rates only for stop loss, because V1's cost calculation mutates the stored columns (Quote.php:9489-9495). Note stop loss is out of scope for Baldwin anyway.

Contributions: skip. /plans/{planId}/contributionrates exists and we are deliberately not using it. Contributions live in the presentation layer and are set per scenario.

How the agent runs

Deterministic where it can be, intelligent where it must be.

Step 1: Learn this instance

Agent · once per BKB instance

Pull the tenant taxonomy: plan types, base plan types, benefit items per plan type, tier types, custom field definitions on both accounts and plans, statuses, and the carrier list. Produce a mapping table and put it in front of a person. This is the artifact that makes every subsequent step cheap, and it is per instance, so Baldwin needs one per BKB instance they run.

Step 2: Enumerate the book

Deterministic runner · no LLM

Page /accounts/search and /plans/search, checkpointing as it goes. Emit a manifest: every employer, every plan, every folder, every file, with ids and hashes. The manifest is the unit of restart, which matters when a run over 5,000 employers dies at hour six.

Step 3: Pull the substance

Deterministic fetch, agent mapping

Per plan: benefit item values, rates, fees. Per employer: contacts, and every document downloaded via /file/download and hash-deduped. Skip census and enrollment files, which are PII we have no reason to hold for a plan conversion.

Step 4: Read the documents

Open Plan

/docid classifies each file and produces the plan association BKB does not have, then /extract-all pulls the facts. Where a document and a BKB plan record describe the same plan, we now have two independent sources, which is the strongest accuracy position available in this project.

Step 5: Emit the Employer Book

Agent · schema-forced

One bundle per employer carrying Open Plan schema facts, provenance, raw source values, and confidence. Plans travel as a package with a group boundary, and every plan also stands alone as a library entry. Then land through PlanfactService as already planned.

What we need from Baldwin

Laura's homework from the Aug 11 call, in the order that unblocks us.

  1. API privileges or tokens, per Steve's ask at 45:22. If a token is not possible, a read-only named user account works for the agent and matches how ThreeFlow appears to have operated.
  2. How many BKB instances, and which regions and offices sit on each. Every instance needs its own step-1 taxonomy pass, so this determines the size of the job.
  3. Which offices are not on BKB at all, and where their plan data lives instead. Those go straight to the document path, which needs nothing from Zywave.
  4. What their export and custom reporting can produce. Worth knowing even with API access, as a cross-check on completeness and a fallback for any instance we cannot reach.
  5. Confirmation on documents. Whether plan summaries and certificates really are filed in BKB per region, since "supposed to be there" is not a plan.

Sources. BKB surface parsed from ~/Documents/Zywave/bkb-openapi.json (53 paths, 225 schemas, pulled 2026-08-08). V1 fields read from ~/Code/plansight-v1 at master a8e7f76c89: app/Models/Group.php $formLabels, app/Models/Quote.php $formLabels, Plantypes.php:1483. Call facts from the Aug 11 recording, timestamps linked inline.

Project state: BrokerageBuilder Integration/HANDOFF.md in the Plansight Brain vault. Deal state: ~/Documents/Baldwin/BALDWIN-STATE.md §0-AUG11.