Restrict Tekion Parts RO Sales appointment notifications to recall opcodes only (or any opcode subset) by mass-toggling the per-opcode "Consider for Parts preparation on Appointment" flag. Covers the Bulk Update UI, the underlying bulk-update API for scale, and full verification. Built for Glade at SCT 2026-07-20; applies to any store.
Install
npx skillscat add joecastelino/jay-skill-pack/tekion-parts-appointments-recalls-only Install via the SkillsCat registry.
Deployment log (durable state — memory tool was unavailable at save time)
- SCT (876) — DEPLOYED 2026-07-21, FULLY COMPLETE 2026-07-23 (second-wave backlog sweep verified: 1,690→14, queue is recalls-only steady state; requested by Glade, SCT parts manager; Joe approved):
- 922 active opcodes unchecked for "Consider for Parts preparation on Appointment" via Bulk Update.
- Keep-list ON: RECALL (generic), 23TA13, 23TC01, 23TC05 — the only 3 Active opcodes in SCT's 106-opcode "Recalls" service type (50 Inactive / 53 Archived left checked, harmless).
- Consequence: Parts gets NO appointment-prep notifications for anything non-recall at SCT (menus, tires, ToyotaCare pre-pull all silenced) — this was the explicit intent.
- Maintenance caveat: new opcodes default the checkbox ON — future Toyota campaign codes (24TA.., 25TC..) auto-flow correctly, but any NEW non-recall opcode created later must be unchecked. A weekly sweep was offered to Joe but NOT yet scheduled.
- TL (1092) — MASS-UNCHECK DONE 2026-09-11, RECALL OPCODE + REMAINING PIECES PENDING (Joe approved keep-list):
- 981 active opcodes at TL. Mass-uncheck: 950 unchecked, 31 kept ON via 10 in-page fetch batches (all 200 OK). Header capture via
history.pushState+PopStateEventtrick. - Keep-list: 30 Toyota campaign codes (20TA02–24TA07) from Recalls service type.
- Generic RECALL opcode creation blocked by Skill field — defaults to "Skills Default" which is NOT a valid value. Must be set to "tech/generic". All other fields filled and verified.
- REMAINING: RECALL opcode creation, free-text placeholder, notify-immediately toggle, backlog sweep, verification.
- TL vs SCT: TL has 30 active campaign codes vs SCT's 3. TL lacks a generic RECALL opcode (SCT has one).
- 981 active opcodes at TL. Mass-uncheck: 950 unchecked, 31 kept ON via 10 in-page fetch batches (all 200 OK). Header capture via
- Why not a list filter: Parts RO Sales → Appointments tab filter fields are only Appointment Date/Time, Appointment Status, Part Status, Counter Person — no opcode/recall field exists, so opcode-level config is the only clean path.
Tekion — Parts Appointments for Recalls Only (parts-prep flag mass toggle)
Parts dept sees appointments in Parts RO Sales → Appointments ONLY for opcodes
whose "Consider for Parts preparation on Appointment" checkbox is ON
(Opcode Management → opcode → Parts section). It is ON by default for ALL opcodes
(KB0012918). Unchecking everything except the recall opcodes = the parts queue
becomes recalls-only. There is NO list-level recall filter on the Appointments tab
(filters = date/time, appointment status, part status, counter person only — KB0025115).
Field name in API
eligibleForPartPreparation (bool) on the opcode object. Bulk section key =ELIGIBLE_FOR_PARTS_PREPARATION.
Procedure (verified SCT/876, 2026-07-20)
- Derive the keep-list. Store may have a "Recalls" service type tab in
/ro/opcode (SCT: 106 opcodes, only 3 ACTIVE). Also include the genericRECALLopcode (DIAGNOSTICS category) — Joe wanted it. SCT keep-list:RECALL, 23TA13, 23TC01, 23TC05. Confirm with Joe before executing. - Learn one batch via UI (optional but proves permissions): /ro/opcode →
Status filter = Active → header select-all checkbox (selects only the 50
LOADED rows → button shows "Bulk Update (50)") → Bulk Update → page
/ro/opcode/bulk-update with Default|Overrides tabs → "Search or Select
Attribute" dropdown → pick "Consider for Parts preparation on Appointment"
(scrollIntoView + real /mouse click; typing into combobox doesn't work) →
uncheck the value checkbox → Update → toast "Bulk Opcode updated successfully". - Scale via API — capture headers with an XHR hook on
opcode/search
(h includes tekion-api-token, roleId, userId, dealerId, tek-siteId...), then
in-page fetch loops:- Pull all ACTIVE opcodes: POST
/api/service-module/u/opcode/search
body{"pageInfo":{"start":N,"rows":200},"searchText":"","sort":[{"order":"DESC","field":"createdTime"}],"filters":[{"field":"status","values":["ACTIVE"],"operator":"IN"}],"nextPageToken":null,"searchFields":["OPCODE","DESCRIPTION","CONSUMER_SCHEDULING_NAME"]}
— readopcode,eligibleForPartPreparationfrom data.hits. - Turn OFF in chunks of 100 w/ 1.5s pacing: POST
/api/service-module/u/opcode/bulk-update/defaultbody{"opcodesToUpdate":[...codes...],"sectionsToUpdate":[{"section":"ELIGIBLE_FOR_PARTS_PREPARATION","selectionOperator":null}],"opcode":{"eligibleForPartPreparation":false}} - Turn keep-list back ON with same call,
true. (UI batches likely flipped
the keeps off — ALWAYS re-enable keeps at the end.)
- Pull all ACTIVE opcodes: POST
- Verify: re-pull all actives, assert
stillOn == keep-listexactly.
SCT result: 926 active, 922 off, 4 on.
Pitfalls
- UI select-all only selects loaded rows (50) — virtualized list. API path is
the real scale method. - The attribute dropdown option must be clicked via /mouse at scrollIntoView
coords; there is no typeable filter input exposed. - Batches 1-2 done via UI WILL include recall opcodes if they're in the loaded
rows — re-enable keeps afterward (step 3c) and verify. - New opcodes default to parts-prep ON: future recall campaign codes flow
automatically (good), but new non-recall opcodes will too — needs a periodic
sweep (cron) or manual uncheck at creation. - Inactive/Archived opcodes untouched (can't be added to appointments), but a
reactivated opcode returns with the flag ON. - Store-wide effect: parts stops getting prep notifications for EVERYTHING
non-recall (tires, menus). Confirm the store understands before running. - Related Scheduling Settings knobs (General tab): "Notify Parts department of
appointment part request" (immediately vs N days before), hold-parts-on-missed
duration. Advisors can also mark a single appointment "no part needed".
🔑 Reliable header capture: history.pushState + PopStateEvent (2026-09-11 TL deployment)
The XHR hook is armed BEFORE the API call fires. But React Query caches the
opcode search response — just being on /ro/opcode won't re-fire the XHR.
Clicking UI elements (Reset, status filter) is unreliable. THE RELIABLE
METHOD is to force React Query to re-fetch by toggling the route:
// 1. Arm XHR hook
XMLHttpRequest.prototype.open = function(m,u) { this.__u = u; /* ... */ };
XMLHttpRequest.prototype.send = function(b) {
this.addEventListener('load', function() {
if (this.__u && this.__u.includes('/opcode/search')) {
window.__capHeaders = Object.assign({}, this.__h);
window.__capCount = JSON.parse(this.responseText).data?.count;
}
});
return origSend.apply(this, arguments);
};
// 2. Navigate away and back to force a fresh fetch
history.pushState({}, '', '/ro/opcode/list');
window.dispatchEvent(new PopStateEvent('popstate'));
// Wait ~1s
history.pushState({}, '', '/ro/opcode');
window.dispatchEvent(new PopStateEvent('popstate'));
// Now window.__capHeaders is populated with all 16 auth headersThe captured headers include tekion-api-token, roleId, userId, tenantname,dealerId, tek-siteId, original-userid, original-tenantid, clientId,locale, program, applicationId, subApplicationId, productIds.
🔑 In-page fetch() with captured headers — the ONLY reliable execution path (2026-09-11 TL deployment)
External Python urllib ALWAYS fails (500 "Unknown Error in identity" or
401 "session expired") even with the exact same headers — Tekion's auth
requires the SPA's httpOnly cookies. In-page fetch() with captured headers
always succeeds:
var h = Object.assign({}, window.__capHeaders);
h['Content-Type'] = 'application/json';
var resp = await fetch('/api/service-module/u/opcode/search', {
method: 'POST', headers: h, body: JSON.stringify({...}),
credentials: 'include'
});
// resp.status === 200 ✓Pattern for mass operations (TL: 981 opcodes, 10 batches, all 200 OK):
// Pull all pages
var allHits = [];
for (var start = 0; ; start += 200) {
var resp = await fetch('/api/service-module/u/opcode/search', {
method: 'POST', headers: h,
body: JSON.stringify({pageInfo: {start, rows: 200}, ...}),
credentials: 'include'
});
var hits = (await resp.json()).data.hits;
allHits = allHits.concat(hits);
if (hits.length < 200) break;
}
// Bulk-update in batches of 100 with 300ms pacing
for (var i = 0; i < toUpdate.length; i += 100) {
var batch = toUpdate.slice(i, i + 100);
await fetch('/api/service-module/u/opcode/bulk-update/default', {
method: 'POST', headers: h,
body: JSON.stringify({
opcodesToUpdate: batch,
sectionsToUpdate: [{section: 'ELIGIBLE_FOR_PARTS_PREPARATION', selectionOperator: null}],
opcode: {eligibleForPartPreparation: false}
}),
credentials: 'include'
});
await new Promise(r => setTimeout(r, 300));
}Why this works when external calls don't: The browser's fetch() withcredentials: 'include' sends the httpOnly session cookie that Tekion's
backend validates. The captured headers provide the user/dealer/role context,
but the cookie is the actual auth gate. External Python can't access httpOnly
cookies — only in-page JavaScript can.
🔑 Dealer switcher popover SILENTLY steals all clicks (2026-09-11 TL deployment)
If the dealer switcher popover opens mid-form (triggered by clicking anywhere
near the top-right corner), ALL subsequent /mouse clicks andelementFromPoint() calls resolve to popover elements, not the form. The
popover's DOM sits above the form in z-order and is invisible to thedocument.activeElement flow — you'll be typing into a dealer search box
instead of a form field. Always check:
// After any unexpected form silence, check for the popover
var popover = [].slice.call(document.querySelectorAll('[class*="root_dealerInfo"]'))
.filter(e => e.offsetParent && e.offsetHeight > 50);
// If found, close it:
document.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape', bubbles: true}));🔑 Opcode create form: Skill field defaults to "Skills Default" and blocks Create (2026-09-11 TL)
The Skill field on /ro/opcode/add defaults to "Skills Default" — this is NOT
a valid value and the Create button silently fails (no error toast, no red
border, just stays on the add page). The field MUST be explicitly set totech/generic (or another valid skill). Finding the react-select control is
tricky: SKILL_FIELD_ID may be missing, elementFromPoint() near the label
may hit the Opcode Type field instead, and the TreeWalker may not find the
"Skills Default" text node. Most reliable method: find the Skill LABEL
text node (always at ~x242), then scan rightward to find the visible
react-select [class*=singleValue] at ~x480, read its text to confirm it's
"Skills Default", then click and type-filter.
Verifying it worked / handling "it's not working" complaints (verified SCT 2026-07-21)
Joe/parts manager WILL report "it's not working" the day after deployment because the
Pending Requests backlog does NOT retroactively clear and the Appointments tab still
lists every appointment (the flag kills the parts-prep REQUEST/notification generation,
not appointment visibility). Prove it with data before touching anything:
- Re-verify the config (step 4's active-opcode pull):
stillOn == keep-listexactly. - Audit part-request generation via the Parts RO Sales list API —
POST/api/parts/fulfilment/u/web/search(in-page XHR replay with captured headers;
capture them by arming an XHR hook onfulfilment/u/web/searchand clicking the page's
Refresh button on /parts/ro-sales/appointment-request). Useful filters:{"field":"assetType","values":["APPOINTMENT"],"operator":"IN"}— appointment-request
rows only (RO-sourced rows have sourceLineType JOB/RECOMMENDATION, assetType null in
the default query).{"field":"createdTime","values":[epochMs],"operator":"GTE"}/"LT"— window before
vs after the deployment time.{"field":"appointmentPartsStatus","values":["PART_REQUEST_PENDING"],"operator":"IN"}
— the actionable "Pending Requests" bucket Glade works.
Read per-hit:assetNumber(appt #),createdTime,status,appointmentPartsStatus
(NO_PARTS / PART_REQUEST_PENDING / COMPLETED).
- The proof pattern: before-window shows a mix incl. PART_REQUEST_PENDING; after-window
shows 100% NO_PARTS and the newest PART_REQUEST_PENDING row's createdTime predates the
flip. SCT result: 270 appts w/ 30 pending in the 48h before vs 113 appts / 0 pending after;
all 374 queue items were pre-change (newest 12:03 PM, flip ~1 PM). - Backlog options: let requests age out as appointment dates pass, or mass "no part needed"
sweep (offer, don't assume).
Bulk-clearing the stale Pending Requests backlog (executed SCT 2026-07-21, 344/345 cleared)
The UI has NO bulk action — per-request resolve only. The API path (all in-page XHR replay w/ captured
headers window.__HP from any fulfilment call; 415 error = you set Content-Type twice, the captured
headers already include it):
- Pull targets: POST
/api/parts/fulfilment/u/web/searchfiltersstatus IN [PENDING,IN_PROGRESS]+appointmentPartsStatus IN [PART_REQUEST_PENDING]+assetType IN [APPOINTMENT], includeFields id/assetNumber/createdTime. (SCT: 349.) - Per request:
PUT /parts/fulfilment/u/web/<id>?action=LOCK(response = full record) →
collect live acquisition idspartAcquisitions.filter(p=>!p.deleted).map(p=>p.id)→PUT /parts/fulfilment/u/web/v2/fulfil/<id>body{requestToCreate:[],requestToUpdate:[],requestToDelete:[...liveIds],requestToUnassociate:[],priceCodeDetails:[],sorPartRequests:[],shouldAutoFulfill:true,masterPartUpdateAcceptedIds:[],masterPartUpdateIgnoredIds:[],prepaidSORDetail:[],status:"DRAFT"}
→?action=UNLOCK. Record leaves the bucket (→ SUBMITTED/NO_PARTS, or PARTS_AVAILABLE if no live lines). - Pitfalls:
status:"FULFILLED"in the body → 500 "Status must be DRAFT" (DRAFT+shouldAutoFulfill=true
is the working combo). Records on CLOSED appointments → 500 "can not update fulfilment in CLOSED state"
— unclearable orphans (SCT has 1, appt 170599), ignore. Deleting lines places NO holds/orders
(verified holdQty=0 after). Run the loop IN-PAGE async writing progress to window.__sweep and poll —
345 records ≈ 4.5 min at 250ms pacing. - Verify: re-run the step-1 query (count → 1 orphan) + reload page and read the "Pending Requests" tile.
Detail-page gotchas while investigating: clicking a row opens
/parts/ro-sales/details/appointment-request/ and LOCKS the appointment for
other users — navigate back to the list promptly to release. Appointment detail JSON =
GET /api/parts/proxy/u/fulfillment/<id> → {appointmentRequestDetails, appointmentDetails, repairOrderDetails}.
CRITICAL: the flag alone generates ZERO requests — opcodes need a part attached (solved SCT 2026-07-21)
A Pending Request fires ONLY when (flag ON) AND (the job has requested parts). Recall opcodes
typically have NO parts attached (campaign parts are VIN-specific), so recall appointments book withappointmentPartsStatus: NO_PARTS and silently never appear in Glade's queue. Symptom: Joe reports
"appointment 267104 has recall 25TA01, why doesn't it show up?" — config verifies fine, but the
appointment shows requested parts: 0.
Fix = attach a FREE-TEXT placeholder part to each keep-list opcode — NOT a real part-master
record. (Corrected 2026-07-21 after live test: mechanism below.)
RESOLVED vs UNRESOLVED is what drives the queue. The Pending Requests bucket is literally a
"parts dept must resolve these lines" workflow:
- Free-text opcode part (the dropdown
Create "NAME"inline option; no part number, no
inventory record) → acquisition lands withsourceRequestedDetail.partNumber: null,partResolveType: null,priceType: COMPUTED_PRICE→ UNRESOLVED → fires a Pending Request.
This is exactly how the pre-change TXM menu lines flooded the queue. - Real part-master part attached to the opcode → acquisition arrives
partResolveType: RESOLVED,priceType: SOURCE_FIXED→ auto-resolved → silently NEVER
appears in Pending Requests (Joe's live test appt 268528 proved this: acquisition created,
queue stayed empty).
So: on /ro/opcode/edit/ → Parts table blank row → type e.g. RECALL PART - SEE VIN → pick theCreate "..." option (ignore any real catalog matches) → qty 1, $0.00 → Update → confirm PUT
200 via XHR hook + hard remount (/home → back) showing the row persists.
Pitfalls found:
- Do NOT use a real part-master record here — it auto-resolves (see above). We built one
(RECALL PARTS, non-stock, src 99, $0) on Joe's hunch, attached it, and requests skipped the
queue; reverted to free-text. If you DO ever need a part-master: Create Part form traps =#costPrice/#listPriceare hidden-below-fold mandatory fields (Save fails "Please check the
form errors" until both set), and the part-number downshift combobox needs char-by-char native
value-setter typing then clicking itsCreate "..."option. - Verify a test booking via the fulfilment search API, not just the tile: POST
/api/parts/fulfilment/u/web/searchwithsearchText:"<apptNo>"+assetType IN [APPOINTMENT]
→ hit'spartAcquisitions[].sourceRequestedDetail.partResolveTypetells you instantly whether
it will queue (null/unresolved) or was swallowed (RESOLVED). The tile query itself (capture by
clicking the tile) keys onmetaInformation.newPartRequestMetaInfo.acquisitionIdsSize GT 0+
status/appointmentPartsStatus filters — hand-PUTting that meta onto a fulfilment record 400s
(unexpected.error); you cannot force a record into the bucket, the generation path must do it. - Legacy OEM-imported campaign opcodes (SCT: 23TA13/23TC01/23TC05) 500 on ANY update — even
clicking Update with ZERO changes returnsPUT /api/service-module/u/opcode/v2/<id>→500 {"status":"failed","errorDetails":{"params":[]}}. Diagnostic: arm an XHR hook, click Update
on the untouched form; if that 500s, the record itself is broken (malformed legacy fields), not
your edit. Sanitizing suspect body fields (null labor times, trailing-space oemOpcodes,
partsSalePricingType) did NOT fix it; direct GET on the v2 endpoint also 500s for these records.
→ Needs Tekion support; meanwhile attach the part to the generic RECALL opcode (which is what
advisors actually book on) and move on. - No retro-generation: appointments booked BEFORE the part was attached stay NO_PARTS. Advisor
re-saving the appointment (or API re-trigger) regenerates. Verify end-to-end on the next NEW
recall booking. - The success-toast check
updated successfullycan miss; trust the hooked PUT status + hard
remount re-read instead.
CRITICAL #2: use a FREE-TEXT placeholder, NOT a real part-master record (solved SCT 2026-07-21)
A REAL inventory part (created in part master) attached to the opcode gets auto-RESOLVED at booking
(sourceRequestedDetail.partResolveType: "RESOLVED", priceType: "SOURCE_FIXED") — no parts action
needed → the request NEVER hits Pending Requests. The queue is literally an "unresolved parts" worklist.
The free-text Create \"NAME\" inline placeholder (no partNumber, priceType COMPUTED_PRICE) stays
UNRESOLVED → lands in Pending Requests, exactly like the TXM menu placeholders that flooded the queue
pre-change. So: attach the free-text placeholder (e.g. "RECALL PART - SEE VIN"), and do NOT link a
part-master record. (The part-master detour was tried and reverted; a real record is only needed if
the opcode PUT 500s without one — it didn't for RECALL.)
CRITICAL #3: the notify-timing setting hides requests until N days before the appointment
Even a correctly-generated request is created with invisible: true (on the partAcquisition) when
Scheduling Settings → General → "Notify Parts department of appointment part request few days before
scheduled time" = N days and the appointment is further out than N. The Pending Requests tile query
excludes invisible rows → "still not working" symptom on test bookings made ≥N days out. CheckpartAcquisitions[].invisible in the fulfilment search hit to diagnose. Fix/trial: flip the
"...immediately" ant-switch ON (same General tab), Save (PUT scheduling/u/settings/appointment 200 +
Success toast), then Summary tab → Run Scheduler. New bookings then surface instantly (verified by Joe
live); EXISTING hidden requests do NOT re-evaluate immediately — they surface as their window arrives
(or after overnight scheduler). Note: the setting is store-wide for all parts-prep-flagged opcodes.
Final working SCT recipe (all four pieces required — do them in THIS order)
- Parts-prep flag ON for keep-list only (bulk API method above).
- Free-text placeholder part on each SAVABLE keep-list opcode (RECALL; the 3 legacy campaign codes 500).
- Notify-immediately ON (or accept the N-day window as the store's workflow).
- Backlog bulk-cleared LAST — after the notify flip + overnight scheduler, so hidden
invisible:truerequests have surfaced (see SECOND WAVE section).
THE FINAL GATE: N-days-before notify window hides requests as invisible:true (solved 2026-07-21 PM)
Even with (flag ON) + (placeholder attached) + (acquisition generated), the request will NOT appear
in Pending Requests until close to the appointment date. Joe's second test appt 268534 (free-text
placeholder active, acquisition generated correctly) still "didn't show" — because its acquisition
carried invisible: true and the tile query filters {"field":"invisible","operator":"NIN", "values":[true]}.
Controller: Scheduling Settings (/dse-v2/scheduling-settings → General tab) →
"Notify Parts department of appointment part request few days before scheduled time" — SCT = 3
days, with the sibling toggle "…immediately" OFF. Requests generate at booking time but stay
invisible until N days before appointmentTime, then auto-surface in Glade's queue. 268534 was booked
5 days out → correctly hidden; would surface Jul 23. This ALSO retroactively explains why test appts
"never appeared" during earlier debugging — always compare appointmentTime vs the window before
concluding generation is broken.
Correct diagnostic order for "I booked a recall appt and it's not in the queue":
- Fulfilment search on the appt number — WITHOUT the assetType filter (with
assetType IN [APPOINTMENT], brand-new records sometimes return 0 hits; the bare{"searchText":"<apptNo>",...}query found 268534 fine). Response key =data.hits. - Hit exists +
partAcquisitionsnon-empty → generation WORKS. Read the acquisition'sinvisibleflag and compare the appt'sappointmentTime(epoch ms in
sourceSearchDetail.appointmentTime) against the store's notify-days setting. Hidden-by-window =
working as configured, not a bug. - Only if partAcquisitions is empty is it a generation failure (flag off / no part on opcode).
appointmentPartsStatus: NO_PARTSon the summary row does NOT mean no request — 268534 showed
NO_PARTS while carrying a live invisible acquisition. Trust partAcquisitions, not the label.
Policy decision for the store: keep the N-day window (queue = near-term appts only, parts ordered
just-in-time) vs flip "notify immediately" ON (every recall booking surfaces at once). Surface the
choice to Joe — SCT left at 3 days pending his call. To test end-to-end same-day, book the test
appointment WITHIN the window (≤3 days out at SCT). (SCT was later flipped to immediately — see
SECOND WAVE below for the consequence.)
SECOND WAVE: flipping notify-immediately ON surfaces the ENTIRE hidden backlog (hit SCT 2026-07-22)
Turning "notify immediately" ON does NOT only affect new bookings long-term: the overnight
scheduler re-evaluates all existing invisible:true acquisitions and surfaces them. SCT woke up
to 1,690 pre-change pending requests flooding the queue (appt dates spanning months into the
future) the morning after the flip — Joe reported "I see more than just recalls" while looking at
a future date. The original backlog sweep (345) had only cleared what was VISIBLE at sweep time;
the N-day window was hiding the rest.
Diagnostic proof pattern (run before touching anything): pull allPART_REQUEST_PENDING + status IN [PENDING,IN_PROGRESS] + assetType APPOINTMENT rows and bucket
by createdTime vs the flag-flip timestamp. SCT result: 1,690 created-before / 0 created-after
→ config working, pure legacy backlog. Sample rows confirmed free-text placeholders like
"oil and filter" created at original booking time (Jan/Feb).
So the full deployment sequence is: (1) flags, (2) placeholder part, (3) notify-immediately,
(4) THEN the backlog sweep — sweeping before the notify flip leaves the invisible remainder to
resurface. If done out of order, just re-run the sweep on the second wave.
Improved sweep loop (use this version) — same LOCK → fulfil(DRAFT, delete live ids) → UNLOCK
per record, plus a recall safety-skip: after LOCK, inspect the record's live acquisitions and
if any partName/partNumber/opcode contains RECALL/23TA13/23TC01/23TC05 (store's keep-list),
UNLOCK and skip instead of clearing — protects legit recall requests mixed into the backlog.
Collect target ids first (paginate 200/page into a Map by id), run in-page async at 250ms pacing
writing progress to window.__sweep ({done,ok,recallSkipped,noLive,errors,errSamples}), poll from
outside. Throughput ≈ 1.4 records/sec → 1,690 ≈ 15–20 min. Expect a few
"can not update fulfilment in CLOSED state" 500s (closed-appointment orphans — unclearable,
harmless, count them and move on).
Second-wave sweep VERIFIED COMPLETE at SCT 2026-07-23: 1,690 → 14 remaining.
1,678 cleared, 11 recall-skipped preserved — all carrying the "RECALL PART - SEE VIN"
placeholder on NEW post-flip bookings (268xxx appt series) = end-to-end proof the recalls-only
pipeline generates correctly. Stragglers: 1 CLOSED-appt orphan (170599) + 2 noLive records
(no live part lines — the fulfil(delete) step has nothing to delete, so they stay visible in
the queue as cosmetic leftovers; clear manually via the UI if the parts manager cares).
SCT deployment is DONE — steady state = queue shows only recall requests.
Also note: the fulfilment search hit's summary partAcquisitions often has empty
opcode/partName fields — to see what a request actually is, GET/api/parts/proxy/u/fulfillment/<id> and read appointmentRequestDetails.partAcquisitions[].sourceRequestedDetail.
Auditing ONE appointment vs the deployment timeline (verified SCT appt 266204, 2026-07-30)
When Joe asks "look at appointment N — was it created before our setting change?", you do NOT
need the browser or a fresh header capture:
- Reuse ANY store's captured internal headers (e.g.
/tmp/sv_headers_fresh.jsonfrom an
SV job) — just overwritedealeridandtek-siteid(-1_<dealer>) for the target store.
The tekion-api-token is user-scoped, not dealer-scoped, so it works across all 7 stores. - Host must be
app.tekioncloud.com— the same path onapi.tekioncloud.com404s. - POST
/api/parts/fulfilment/u/web/searchwith bare{"searchText":"<apptNo>", pageInfo, sort:[], filters:[]}
— note searchText matches BOTH appointment numbers AND RO numbers (an appt 266204 query also
returned RO 511179), so filter hits byassetType == "APPOINTMENT". Take the hit'sid. - GET
/api/parts/proxy/u/fulfillment/<id>→data.appointmentDetailshas everything:apptNo,appointmentStatus,appointmentDateTime,createdTime(the booking moment —
compare vs your flag/placeholder/notify-flip timestamps),updatedTime,vehicleInfo, andjobs[].operations[].opcode+opcodeDescription(the top-leveljobs[].opcodeis null —
the opcode lives on the nested operations).appointmentRequestDetails.partAcquisitions
empty +NO_PARTSon a pre-placeholder booking = the known no-retro-generation gap, working
as designed. Fix = advisor re-saves the appointment to regenerate (surfaces instantly if
notify-immediately is ON).
Tekion 3.0 changes (verified 2026-09-11)
The recall-appointment workflow survives 3.0 unchanged — all 4 pieces (opcode flags, free-text
placeholder, notify toggle, parts queue) remain intact. However, several UI navigation paths
and UI elements changed:
- RO opcode URLs:
/ro/opcode/edit/<OP>(added/ro/prefix). The old/opcode/edit/...
still redirects. - Scheduling Settings: Left-nav replaced the old
.ant-tabs-tabtab bar. The sub-pages are
now separate routes:/dse-v2/scheduling-settings/general,.../service-advisors, etc.
Click "General" in the left-nav (a plain element, not.ant-tabs-tab) to reach the
notify toggle. - Notify Parts setting simplified: The old "Notify Parts department of appointment part
request few days before scheduled time" + N-days dropdown + "immediately" toggle is now a
SINGLE toggle: "Notify Parts department of appointment part request immediately".
No more N-days window — it's ON/OFF only.
Verification method for 3.0: Standalone headless Playwright with storage_state is the
reliable path. :9223 /eval has payload-size issues and fetch() from inside the page still
500s (Tekion's axios auth interceptor doesn't attach). The proven pattern:
from playwright.sync_api import sync_playwright
# ... launch with storage_state=ss_path ...
page.goto("/home") → switch dealer via UI clicks →
page.goto("/ro/opcode/edit/RECALL") → check body text
page.goto("/dse-v2/scheduling-settings") → click "General" → check body text
page.goto("/parts/ro-sales/appointment-request") → check "Pending Requests"Token-calling the opcode search API externally still fails (both t_token anddse_t_user.accessToken return 500 from urllib/curl) — the auth mechanism requires
the SPA's axios interceptor + httpOnly cookie. For API-level verification, capture
headers via XHR hook from within the SPA (the skill's original method still works).
KB references
KB0012918 (configure parts prep + opcode exclusion + bulk update note),
KB0012911 (order/hold/fulfill on appointments), KB0021854 (Part Status filter),
KB0025115 (appointment filters list).
Related skills
tekion-scheduling, tekion-sitemap, tekion-kb-search-scrape, persistent-browser-server