Workiz Integration
Workiz is an FSM (field-service management) platform aimed at small-to-mid-size service businesses (locksmiths, HVAC, appliance repair).
Everything JB360 does against the Workiz API. Workiz is platformId 2 in the onboarding wizard (ServiceTitan is 1); a dealer is a Workiz dealer when crmSystem = "Workiz" (tblDealerOnboard/tblDealer) and a row exists in tblWorkizAccounts. The integration lives entirely in jbw-sam (Node Lambdas) and jbwui (React) — the Java modules (extraction/, jbw-api/) contain zero Workiz code.
:::
- Shared static Workiz API token hardcoded in source — in the URL path of all three callers (
jbwworkizjobsdatafetch/index.js:62,jbwworkizaccountverify/index.js:10,jbwagreementsdatamatch/index.js:3); it is in no.env, and rotation means code change + redeploy. JBW_API_TOKENin plaintext intemplate.yaml:1115.- The production pipeline depends on untemplated Lambdas (
prod_jbwworkiztenantifnoproducer,prod_jbwworkizjobdatapushpush,jbwworkizjoblocationsdatapull) wired outside the template. All three now have source (added 2026-07-16), but none is intemplate.yaml, so the repo alone still cannot rebuild this integration. Details in Sync & Data Flow.
Connection Model
Section titled “Connection Model”- API base:
https://api.workiz.com/api/v1/ - Auth: a single shared API token embedded in the URL path (
…/api/v1/jb_warranties_7a9f…/) — no OAuth, no token exchange, no per-dealer credentials (hardcoded; see the Security & drift callout above). - Per-dealer scoping: the dealer’s Workiz Account ID (captured at onboarding, stored in
tblWorkizAccounts) is sent as an HTTP header. ⚠ The header name is spelled inconsistently:Account-Id(jobs fetch, locations fetch) vsaccount_id(account verify). - No tenant concept: Workiz dealers reuse
dealerIdastenantIdeverywhere (jbwdealeradmintoolsupdate/index.js:182-185,jbwworkizjobsdatasave/index.js:6-7). - Portal deep-links (UI):
REACT_APP_WZ_URL— prod/devhttps://app.workiz.com/root/job/, stagehttps://jbwarrantiestests.tests.workiz.com/root/job/.
Endpoint Inventory
Section titled “Endpoint Inventory”All three calls are GET; there is no Workiz write-back.
| Endpoint | Used by | Purpose |
|---|---|---|
GET /job/all/?status=done&utc_updated={syncStartDate}&limit={n}&offset={n} | jbwworkizjobsdatafetch (index.js:62-70) | Incremental pull of completed jobs since last sync → invoices/line-items |
GET /job/all/ | jbwworkizaccountverify (index.js:10-18) | Onboarding: validate the dealer’s Account ID (response.code 200 → “Successful”, 401 → error msg) |
GET /job/getLocationsWithLatestJobs/?limit={n}&offset={n} | jbwagreementsdatamatch Workiz branch (index.js:435-447) | Pull all job locations → tblWorkizLocationData for agreement/location matching |
Pagination contract (jobs + locations): response carries flag (truthy = valid), data (array), has_more; the loop bumps offset by limit (default 50) until has_more !== true (jbwworkizjobsdatafetch/index.js:168-201, jbwagreementsdatamatch/index.js:511-544).
Lambdas & Triggers
Section titled “Lambdas & Triggers”| Lambda | Trigger | What it does |
|---|---|---|
jbwworkizaccountverify | API GW GET /workiz/accountverify (template.yaml:1607) | Validates an Account ID against Workiz during onboarding. No DB access. |
jbwworkizjobsdatafetch | SQS (reads event.Records[0].body) — ⚠ no event source declared in template.yaml (:1632-1654); wired out-of-band | Pulls done jobs (paginated), maps them to invoice objects (parseWorkizJsonData), invokes the save Lambda synchronously via JBW_WORKIZ_JOBS_DATA_SAVE |
jbwworkizjobsdatasave | Lambda invoke only (template.yaml:1614-1631) | Upserts tblInvoice + tblInvoiceItem (line items with Type==="equipment" → isInstalledEquipment=1), updates tblAgreementEquipments (manufacturer/model), bumps tblTransmissionDetails (+History, destinationSystem='Workiz') |
jbwagreementsdatamatch | Lambda invoke only (template.yaml:1100-1124) | Shared ST/Workiz matcher. Workiz branch (crmSystem==="Workiz", index.js:19): repopulates tblWorkizLocationData (500-row batches), pulls JBW agreements → tblJBWAgreementData, then CALL jbw_st_dataSync(dealerId,'Workiz') |
jbwstdatasyncspcall | API GW GET /agreementsync/{id}/spcall (template.yaml:1148) | On-demand re-sync; picks the Workiz path when the dealer row has an accountId (index.js:81-96) |
jbwdealeradmintoolsupdate | PUT /dealer/{id}/admtools — ⚠ route/function absent from template.yaml (deployed outside IaC) | Admin tools (status/invoice/lastsyncdate/…); for Workiz lastsyncdate it invokes GET_WORKIZ_TENANT_INFO — see gaps below |
Onboarding (per new Workiz dealer)
Section titled “Onboarding (per new Workiz dealer)”The wizard is 4 steps for Workiz vs 5 for ServiceTitan — no client secret, no custom fields/tags:
ClientPlatform.jsx:20— pick Workiz (platformId 2) → routes to the Account ID step.ClientAccountId.jsx— dealer enters their Workiz Account ID; “Connect to Workiz” callsGET /workiz/accountverify?accountId=…and only proceeds on"Successful"(:92-98).CLientConnect.jsx— “Onboard” posts toPOST /newdealeronboard.jbwnewonboarding/index.js— discriminates purely onaccountIdpresence (platform = accountId ? "Workiz" : "ServiceTitan",:47): insertstblDealerOnboard(crmSystem='Workiz'),tblWorkizAccounts(dealerId, accountId), updatestblDealer.crmSystem, insertstblTransmissionDetails(destinationSystem='Workiz', tenantId=dealerId), then invokesjbwagreementsdatamatchfor the first sync. The ST-onlytblTenant/tblTenantCustomTags/tblTenantCustomFieldsinserts are skipped (:75-80).
Config stored for a Workiz dealer: tblDealerOnboard, tblWorkizAccounts, tblDealer.crmSystem, tblTransmissionDetails — no clientId/secret/tenantId/custom fields/tags.
Sync & Data Flow
Section titled “Sync & Data Flow”| Flow | Trigger | Path |
|---|---|---|
| First sync (onboarding) | jbwnewonboarding → jbwagreementsdatamatch | Locations → tblWorkizLocationData; JBW agreements → tblJBWAgreementData; CALL jbw_st_dataSync(dealerId,'Workiz') |
| Manual re-sync | UI → GET /agreementsync/{id}/spcall | Same matcher, Workiz branch when accountId present |
| Job/invoice pull | prod-workizdatapull (daily) → prod_jbwworkiztenantifno (untemplated) → SQS prodWorkizTenants → jbwworkizjobsdatafetch → jbwworkizjobsdatasave | Done-jobs incremental pull (utc_updated) → tblInvoice/tblInvoiceItem → agreement-equipment update → transmission bookkeeping |
| Job push (write-back) | prod-workizdatapush (daily) → prod_jbwworkizjobdatapush (untemplated), fed by vwWorkizDataPush | Pushes agreement/claim data into Workiz — source at jbw-sam/src/jbwworkizjobdatapush/ since 2026-07-16 |
| Location pull | jbwworkizjoblocationsdatapull (untemplated) → tblWorkizLocationData | ⚠ Cannot persist as written — see caveats |
ServiceTitan has getTenantInfo → SQS as the scheduled producer. The Workiz analogue is jbwworkiztenantifno (the directory name is misspelled in the repo) — it reads tblWorkizAccounts joined to tblTransmissionDetails and enqueues one SQS message per active Workiz account to process.env.QUEUE_URL (jbw-sam/src/jbwworkiztenantifno/index.js:30-32), with a different message shape from getTenantInfo (dealerId, accountId, batchId, syncStartDate — no clientId/clientSecret/tenantId).
This makes it a second, independent enqueuer into the extraction pipeline — a fact the module docs missed entirely until 2026-07-17.
Prod runs these untemplated Lambdas (Schedules & Queues, Lambda Inventory):
prod-workizdatapull(daily) →prod_jbwworkiztenantifno— the producer that feeds queueprodWorkizTenants→prod_jbwworkizjobsdatafetchprod-workizdatapush(daily) →prod_jbwworkizjobdatapush— the write-back into Workizjbwworkizjoblocationsdatapull— the location pull
All three gained source on 2026-07-16; none is in template.yaml. The env var GET_WORKIZ_TENANT_INFO that jbwdealeradmintoolsupdate/index.js:34-43 invokes, and the /dealer/{id}/getworkizinfo route the UI calls (dealerServices.js:46-52), are still not declared in template.yaml — they are wired in the console.
jbw-sam/src/jbwworkizjoblocationsdatapull/index.js:62-91 has three compounding bugs in saveWorkizLocationsData:
const result = '';(:68) is reassigned at:75→TypeError: Assignment to constant variable- it calls callback-style
connection.query(...)on amysql2/promiseconnection connection.commit()(:78) andreturn result(:79) run before the insert callback fires, andfinally(:89) closes the connection underneath it
It also reads event.body.accountid without JSON.parse (:9), so accountId is undefined for a real API Gateway proxy event, and passes dealerId for both tenantId and dealerId (:113-114).
Whether this is live or dead code is unresolved — the function is deployed (last modified 2025-05-13 dev / 2026-04-17 prod) but jbwagreementsdatamatch also populates tblWorkizLocationData, so the documented flow may work regardless. Check CloudWatch Logs for the function to settle it.
prodWorkizTenants and stgWorkizTenants have no dead-letter queue — failed job pulls are silently dropped after retries.
UI Differences (Workiz vs ServiceTitan dealer)
Section titled “UI Differences (Workiz vs ServiceTitan dealer)”| Where | Difference |
|---|---|
DealerDetails.jsx:178 | Tenants panel hidden (no tenant concept) |
AgreementsGrid.jsx:570-577, 630 | Invoice links go to WZUrl + invoiceId; “New Agreement” button ST-only |
ClaimsGrid.jsx:661-668 | Product-code links go to WZUrl |
EquipmentsTable.jsx:334 | Equipment selection checkboxes ST-only |
UserRegistrationDialog.jsx:516-520 | tenants/businessUnits sent empty for Workiz |
DealerInfo.jsx:32-35 | Workiz logo rendered from crmSystem |
DealerForm.jsx:85 | New dealers are hardcoded crmSystem:'ServiceTitan' — Workiz is only assignable via the onboarding wizard |
Secrets & Env
Section titled “Secrets & Env”The credential risks are consolidated in the Security & drift callout at the top of this page; the table below is the location index.
| What | Where | ⚠ |
|---|---|---|
Workiz API token (jb_warranties_7a9f…) | Hardcoded in 3 source files (see Connection Model) — not in any .env | Hardcoded credential |
JBW_API_TOKEN | Plaintext in template.yaml:1115 (env of jbwagreementsdatamatch) | Plaintext secret in IaC |
JBW_WORKIZ_JOBS_DATA_SAVE | template.yaml:1648 — target of the fetch→save invoke | |
GET_WORKIZ_TENANT_INFO | Referenced in jbwdealeradmintoolsupdate/index.js:35 | Never defined anywhere — dangling |
REACT_APP_WZ_URL | jbwui .env/stage.env/prod.env |
Behavioral Notes & Pitfalls
Section titled “Behavioral Notes & Pitfalls”constreassignment bug masks failures:jbwworkizjobsdatasave/index.js:11declaresconst syncStatus = "Success"; the catch block assignssyncStatus = "Failure"(:117) — any failure throwsTypeErrorand buries the original error.- NPE risk:
getStateCode(jbwworkizjobsdatasave/index.js:495-500) callsstateName.lengthwithout a null check — a job with nullStatethrows. - Dead code: commented-out
warrantyline-item handling (jbwworkizjobsdatasave/index.js:85-101) and an unused duplicateparseWorkizJsonData(:404-485); the ST-style custom-fields UI is commented out inClientAccountId.jsx.
- SQL injection surface: Workiz Lambdas build SQL via string interpolation of API data (
jbwnewonboarding/index.js:158-161,jbwagreementsdatamatch/index.js:500-502);replaceSpecialCharactersonly partially mitigates the locations path.
- Equipment is inferred, not pulled: there is no Workiz equipment API in use — equipment comes solely from job line items with
Type === "equipment"; amissingEquipmentsflag is computed per invoice.
Asymmetries vs ServiceTitan (summary)
Section titled “Asymmetries vs ServiceTitan (summary)”| ServiceTitan | Workiz | |
|---|---|---|
| Auth | OAuth2 client-credentials per tenant | One shared static token, Account-Id header |
| Direction | Pull + rich write-back (tags/fields/notes/warranty dates/claims) | Pull + write-back via jbwworkizjobdatapush (untemplated) |
| Equipment | installed-equipment API | Inferred from job line items |
| Custom fields/tags | Required, verified at onboarding | None |
| Tenant model | Distinct ST tenantId | dealerId doubles as tenantId |
| Onboarding | 5 steps (creds + custom fields) | 4 steps (Account ID only) |
| Scheduled pull producer | prod_getTenantInfo → SQS | prod_jbwworkiztenantifno → SQS — deployed but not in repo |