What is actually in there, how it maps to V1, and why we send an agent instead of building a connector
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.
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:
| Why | What 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.
Parsed from the live OpenAPI spec: 53 paths, 225 schemas.
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.
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.
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.
GET /plans/{planId}/rates, plus fees and self-funded rate metadata. Six fixed rate
shapes. TieredRate is tier type, count, monthly fee.
GET /folder for the folder tree, POST /file to search, and
GET /file/download for the bytes. Name, MIME type, size, folder, dates, description.
POST /accountcontacts/search for employer contacts, POST /carriers/search
plus carrier codes for the carrier crosswalk.
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.
V1's Group is DynamoDB-backed and schemaless; the field list below is its own $formLabels enumeration.
BKB AccountInfo | V1 Group | Note |
|---|---|---|
name | name (Employer Name) | Direct |
address1, address2, address3 | address1, address2 | ⚠️ BKB has three lines, V1 has two. Concatenate 2 and 3. |
city, stateProvince, zipPostalCode | city, state, zip | Direct |
county | county-fips | ⚠️ BKB gives a county name; V1 wants county plus FIPS. Needs a lookup, and split-ZIP care. |
phone | phone | Direct. fax has no V1 home. |
url | website / authorizedDomain | Direct |
sicCode | sicCode | Direct, but Baldwin flagged this as commonly missing |
numberOfEmployees | employeeCount (Est. Employee Lives) | Also derive marketSize from it |
clientCode | intId (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. |
producerId | primarySalesLead (Producer) | Needs a BKB-user to V1-user crosswalk |
accountStatus, brokerageBuilderStatus, isBenefitClient, isBenefitProspect | clientStatus | Collapse four signals into one enum. Decide the rule once. |
businessLine, benchmarkIndustry, classification | primaryIndustry | Loose. Agent judgment, low confidence, review. |
customFields | varies | Inspect 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, country | none | Drop |
| V1 field | Where 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. |
primaryRenewalMonth | Derive from the plans: the modal planYearStartDate month across the employer's plans. |
acaRatingArea | Derive from ZIP. Watch split ZIPs. |
contact, contactTitle, contactPhone, contactEmail | POST /accountcontacts/search, then pick a primary. Rule needs deciding. |
naicsCode, corpStructure, companyType, dbaList | No source. Leave empty. |
benAdmin, benManagement, benClientId | No source. Baldwin uses Employee Navigator, so this may be fillable later from that side. |
borDate, logo, aboutEmployer, marketing* | No source. Not needed for conversion. |
Scope is the three questions: who is the carrier, what are the rates, what are the plan attributes.
BKB PlanInfo | V1 Quote | Note |
|---|---|---|
planName | name (Plan Name) | Direct |
accountId | groupId (Employer) | Via the account crosswalk |
issuingCarrierId | carrierId (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. |
billingCarrierId | mguCarrierId (Carrier Paper) | Only when it differs from the issuing carrier |
carrierPolicyNumber | inForcePolicyNumber (Policy / Group #) | Direct |
planYearStartDate | inForceStartDate (Effective Date) | Direct |
planYearEndDate | inForceEndDate (Renewal Date) | Direct. Set useDateRange accordingly. |
planTypeId → PlanType.name / amsCode | insuranceType (Plan Type) | The per-tenant mapping table. Anchor on basePlanTypeId plus amsCode. Target V1's 36-type registry in config/plantypes.php. |
rateType | planType (Funding Type) + rateTierSchedule | ⚠️ rateTierSchedule (string) and rateTiers (int) must both be set |
currentSubscribers | enrollment | Useful sanity check against the census |
planCode | intId | Plus store BKB planId for re-runs |
notes | notes | Direct |
renewedFromPlanId | none | Do 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, departmentId | none | Commission 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.
Deterministic where it can be, intelligent where it must be.
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.
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.
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.
/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.
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.
Laura's homework from the Aug 11 call, in the order that unblocks us.
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.