CustomLinc TOL → Queue-it Availability JSON
Overview
| Workflow |
|---|
T76MXW3QRGcoIBbo3c51da4e-7cc1-4efc-bd6f-eaeff2974593https://n8n.pbr.org.au/webhook-test/availability/tolworkflow/T76MXW3QRGcoIBbo
TriggerHTTP GET/POST webhook
Season2026-06-26 → 2026-07-19 (24 dates)
ProductTOL (18:00 Lakeside Twilight Train)
Base capacity256 pax / date
WC capacity3 slots / date
StatusActive (published)
Manifest.maxPurpose
This n8n workflow exposesis athe JSONbackend endpointfor summarisingPBR's currentQueue-it availabilityintegration for the 2026 TOL (Train of Lights) 2026 season. ItQueue-it queries this endpoint to determine which dates are sold, limited, or available, and uses the data to gate the onsale queue appropriately.
The endpoint combines two independent data sources:
- CustomLinc
MSSQLavailability API —sourcereal-timeofcheckouttruth for departure capacityavailability ():Manifest.maxapiPaxandwhatconfirmedthebookingnextpaxcustomer(viacanactuallyTxnandbookrightBookingtables).now - CustomLinc
RESTSQLAPI(Txn/Booking tables) —sourceconfirmedofbookingoperator-settruth:statewhatsuchhasasactuallyschedulebeen(isAvailable)paid andrun-statusisflagsgenuinely(availabilityStringvaluesnodeparture/noservice).sold
TheThis endpointdual-source producesapproach ais Queue-it-stylerequired because the CustomLinc availability JSONAPI (withis a dynamicMessage string for operators to paste into Queue-it) plus a monitoring block usedinfluenced by thein-cart Grafana(pending) TOLbookings, Onsaleoperator dashboardsales caps, and forManifest opscounter visibilitydrift during— activenone onsaleof periods.which should drive the public SOLD/LIMITED classification.
ArchitectureArchitecture: andNode data flowFlow
Twelve nodes, one webhook trigger, three execution branches: cached response, full live build, and per-chunk API loop.
Availability Webhook
(GET)
↓
Check Cache — reads workflow staticData.availability, computes age vs 30s TTL
↓
Cache Fresh? (IF, true if cached && cachedAt within 30s)
—true→ Return Cached → Respond JSON
—false→ CustomLinc Login (POSTparallel)
/security/login)
↓→ Query Bookings (MSSQL,SQL returnsvia 24Microsoft rowsSQL bynode)
date)
↓→ Ensure One Item (passthrough bridge- forensures emptyClassify SQL)fires ↓even if SQL returns 0 rows)
→ Build Date Chunks (splits season into 10-day chunksAPI fromrequest today)chunks)
↓→ Loop Chunks
(splitInBatches, batchSize=1)
===each batch===→ Fetch CustomLinc Availability (POST /product/availabilitytable)per ↓chunk)
(back→ to[loop Loopback]
Chunks for next batch)
===done===→ Classify and(Code Cachenode (Code:- mergescross-joins SQL+SQL + API, buildsoutputs payload,payload)
writes cache)
↓→ Respond JSON (HTTP 200 with JSON body)
The CustomLinc Login and Query Bookings nodes run in parallel at workflow start. Both results are available by the time Classify executes after the loop completes.
Node-by-nodeSQL referenceQuery: Query Bookings
AllThe nodeSQL parametersquery belowruns against the CustomLinc MS SQL database (subscriber 22). It joins Manifest (per-date capacity) against confirmed (statusType=0) and pending (statusType=6) Txn/Booking records.
Key design decisions
statusType IN (0, 6) — includes both Confirmed (0) and Pending Web Payment (6) bookings as separate columns.
WC (Wheelchair Add-on fare code) is counted separately from standard pax (ADULT, CHILD, TODDLER).
Booking counts are 1.Output Availability Webhookcolumns
| Description |
|---|
n8n-nodes-base.webhooktravel_dateDate availability/tolmanifest_totalPaxManifest counter (cumulative, never decremented on cancel — known drift issue)
responseNodeavailable 2.Manifest Checkcounter Cachedrift
ReadsThe Manifest.totalPax counter increments on booking but does not decrement on cancellation. This is a confirmed CustomLinc bug. The endpoint does not rely on Manifest counters for classification — it uses direct Txn aggregation instead. Manifest mismatches are flagged in monitoring.manifestMismatches[] for ops awareness only.
Classification Model: Pending-Blind
Sold/limited classification is driven exclusively by confirmed seats (statusType=0). Pending bookings (statusType=6, i.e. in-cart / Pending Web Payment) are tracked and reported in monitoring but do not affect the per-workflowpublic classification staticData.availabilityobject and decides whetheror the cacheddynamic payload is fresh (within 30 seconds).
const staticData = $getWorkflowStaticData('global');
const ttlSeconds = 30;
const cached = staticData.availability;
const now = Date.now();
const isFresh = cached && cached.cachedAt && (now - cached.cachedAt) / 1000 < ttlSeconds;
return [{ json: { isFresh: !!isFresh, cached: cached || null, now, startedAt: now } }];Note: staticData is per-workflow process state; it survives between executions but is lost on n8n restart. startedAt is captured here so the Classify node can compute executionDurationMs.message.
3.Classification Cacheinputs Fresh?per (IF)
Single boolean condition: $json.isFresh === true. True branch goes to Return Cached, false branch goes to CustomLinc Login.
4. Return Cached
Strips the internal cachedAt field, adds cacheAgeSeconds to monitoring.health, and sets source: "cache".
const cached = $json.cached || {};
const { cachedAt, ...payload } = cached;
const ageSeconds = cachedAt ? Math.round((Date.now() - cachedAt) / 1000) : null;
const monitoring = payload.monitoring || {};
monitoring.health = {
...(monitoring.health || {}),
cacheState: 'cache',
cacheAgeSeconds: ageSeconds,
};
return [{ json: { ...payload, monitoring, source: 'cache' } }];5. CustomLinc Logindate
| Source | Description |
|---|
n8n-nodes-base.httpRequestpaxConfirmedSQL paxPendingSQL statusType=6Seats in active checkout carts
https://api.customlinc.com.au/puffingbillyrailway/security/loginapiPaxCustomLinc APISeats available to buy right now (reduced by confirmed + pending + any operator cap)
{}isAvailableAPI agentAPIKeyeffectiveCap derivation
ReturnsIf the API reports fewer seats than , {max authToken:- "<token>"confirmed - pending...an }.operator-set Thesales tokencap is consumed by the Build Date Chunks node and forwarded into each Fetch call.
6. Query Bookings (MSSQL)
Single query against the CustomLinc MSSQL database. Returns 24 rows — one per TOL departure date in the season window. Filter is b.statusType = 0, which covers all sold-class booking statuses (Confirmed, Boarded, Gift Voucher redeemed, Confirmed - Outstanding balance).inferred:
WITH dates AS (
SELECT date AS travel_date,
SUM([max]) AS pax_max,
SUM(totalPax) AS manifest_totalPax,
SUM(totalAvailable) AS manifest_totalAvailable
FROM Manifest WITH (NOLOCK)
WHERE uniqueIdSubscriberremainingIfNoCap = 22max(paxMax - paxConfirmed - paxPending, 0)
if isAvail AND codeapiPax < remainingIfNoCap:
effectiveCap = 'TOL'apiPax AND+ datepaxConfirmed >=+ '2026-06-26'paxPending
ANDelse:
date < '2026-07-20'
GROUP BY date
),
bookings AS (
SELECT CAST(b.dateTimeOfTravel AS date) AS travel_date,
SUM(CASE WHEN ft.code IN ('ADULT','CHILD','TODDLER') THEN t.quantity ELSE 0 END) AS pax_booked,
SUM(CASE WHEN ft.codeeffectiveCap = 'WC'null
effectiveCap t.quantityis ELSEsuppressed when !isAvail (stop-sold) because in that case apiPax=0 END)is AScaused wc_booked,by COUNT(DISTINCToperator b.uniqueId)action, ASnot booking_counta FROMcap.
confirmedRemaining (NOLOCK)classification INNERsource)
effectiveCeiling = t.uniqueIdBookingeffectiveCap INNER?? JOINpaxMax
FareType ft WITH (NOLOCK) ON ft.uniqueIdconfirmedRemaining = t.uniqueIdFareTypemax(effectiveCeiling WHERE- t.uniqueIdSubscriber = 22
AND t.type = 'Booking'
AND b.uniqueIdSubscriber = 22
AND b.productCode = 'TOL'
AND b.statusType = 0
AND b.dateTimeOfTravel >= '2026-06-26'
AND b.dateTimeOfTravel < '2026-07-20'
AND ft.code IN ('ADULT','CHILD','TODDLER','WC')
GROUP BY CAST(b.dateTimeOfTravel AS date)
)
SELECT d.travel_date,
d.pax_max,
d.manifest_totalPax,
d.manifest_totalAvailable,
COALESCE(bk.pax_booked,paxConfirmed, 0) AS pax_booked,
COALESCE(bk.wc_booked, 0) AS wc_booked,
COALESCE(bk.booking_count, 0) AS booking_count
FROM dates d
LEFT JOIN bookings bk ON bk.travel_date = d.travel_date
ORDER BY d.travel_date;
Field semantics
pax_max
Manifest.maxmanifest_totalPaxManifest.totalPaxmanifest_totalAvailableManifest.totalAvailablepax_max - manifest_totalPaxpax_bookedTxn.quantitywc_bookedTxn.quantitybooking_countCOUNT(DISTINCT Booking.uniqueId)Why statusType = 0 instead of status = 'Confirmed'
The Booking table has 19 distinct status values across 5 statusType codes. statusType = 0 covers the four "sold and counts against capacity" statuses:
ConfirmedBoardedGift Voucher redeemedConfirmed - Outstanding balanceFor TOL 2026 in its current state the two filters return identical results (all bookings are status Confirmed), but is statusType = 0confirmedRemainingrobustthe againstkey legitimateclassifier seat-occupyinginput. statusesIt appearingrepresents later.how many confirmed seats are genuinely still available, ignoring pending carts.
7.Classification Ensure One Item
Passthrough bridge to guarantee downstream Code nodes always fire even if SQL returns zero rows. The Classify node reads the SQL output via $('Query Bookings').all() rather than via direct input.
return [{ json: { sqlRowCount: $input.all().length } }];8. Build Date Chunks
Splits the remaining season window into 10-day chunks starting from today (or the season start, whichever is later). The CustomLinc availability API requires a start date plus a day count; chunking limits the per-call payload size.
const SEASON_START = '2026-06-26';
const SEASON_END = '2026-07-19';
const CHUNK_DAYS = 10;
const loginItems = $('CustomLinc Login').all();
const authToken = loginItems[0]?.json?.authToken;
if (!authToken) throw new Error('No authToken from login step');
const today = new Date(); today.setUTCHours(0, 0, 0, 0);
const seasonStart = new Date(SEASON_START + 'T00:00:00Z');
const seasonEnd = new Date(SEASON_END + 'T00:00:00Z');
let cursor = today > seasonStart ? today : seasonStart;
const chunks = [];
while (cursor <= seasonEnd) {
const remainingMs = seasonEnd.getTime() - cursor.getTime();
const remainingDays = Math.floor(remainingMs / 86400000) + 1;
const days = Math.min(CHUNK_DAYS, remainingDays);
chunks.push({
json: { authToken, dateFrom: cursor.toISOString().slice(0, 10), days },
});
const next = new Date(cursor);
next.setUTCDate(next.getUTCDate() + days);
cursor = next;
}
return chunks;For the 24-date 2026 season (26 Jun → 19 Jul) this produces three chunks: 26 Jun + 10 days, 6 Jul + 10 days, 16 Jul + 4 days.
9. Loop Chunks (Split In Batches)rules
| Result |
|---|
n8n-nodes-base.splitInBatches!isAvail (sold[] + stopSold[]
confirmedRemaining size=== 0sold[] (confirmedRemaining <= 20limited[]
confirmedRemaining > 20plenty (not in any list)
apiBlocked flag (monitoring only)
isApiBlocked = (apiPax === 0) AND (confirmedRemaining > 0) AND isAvail
ForWhen eachpending iteration,bookings fill remaining seats, the chunkAPI routesreturns toapiPax=0 Fetchbut CustomLincconfirmed Availability,seats whichremain. thenThis routesis backan toops Loopsignal Chunks.— Oncepending allbookings chunkswill are processed, the "done" branch routes to Classifyexpire and Cache.release those seats. The date does not appear in sold[] or dynamicMessage. It is reported in monitoring.apiBlocked[].
10.boundBy Fetch CustomLinc Availabilityfield
| Meaning |
|---|
n8n-nodes-base.httpRequest"capacity"No "confirmed"Confirmed bookings are the limiting factor
https://api.customlinc.com.au/puffingbillyrailway/product/availabilitytable"cap"Operator-set {Wheelchair "authToken": "{{ $json.authToken }}",
"productCode": "TOL",
"date": "{{ $json.dateFrom }}",
"days": {{ $json.days }},
"totalPax": 1,
"returnUnavailableDates": true
}
The response contains a productAvailability[0].availability[] array, one entry per date. Each entry includes classification (dateMicrosoft /Date(epoch_ms)/ format), pax (live availability), isAvailable, and availabilityString.
11. Classify and Cachepending-blind)
TheWC core merging and classification node. Reads SQL output (24 date rows) plusfollows the threesame APIpending-blind chunk responses, produces the final payload, and writes it to staticData.availability with a cachedAt timestamp.
Constants
LIMITED_THRESHOLDWC_CAPWC_LIMITED_THRESHOLDSQL_EXPECTED_ROWSNOT_RUNNING_VALUES{'nodeparture', 'noservice'}availabilityStringPer-date computation
For each API entry:remaining.
/Date(epoch_ms)/to an ISO date, build a"D/M"token (e.g.27/6).
availabilityStringnodeparturenoservicedatesNotRunningremaining = max(pax_max - pax_booked, 0)isAvailable=falseremaining=0isAvailable=falseavailabilityString0 < remaining ≤ 20wc_remainingwcConfirmedRemaining = max(3 - wc_booked,wcConfirmed, 0)wcSold= dateSold ? 0 : wcConfirmedRemaining
if wcLimited<= 0 → wheelchairSold[]
if pax_booked - manifest_totalPaxdatesManifestMismatchbyDate[token]Inflight calculation (with guard)
The CustomLinc availability API pax field returns capacity minus all in-flight bookings (carts in checkout, pending payment). Subtracting our confirmed remaining from their live pax should give the in-flight count:
inflight = max(remaining - apiPax, 0)
Guard: when the API returns apiPax = 0 alongside confirmed bookings (a stop-sold or cache-lag scenario), naive subtraction produces a misleadingly large inflight figure (e.g. 246 on a date with 10 confirmed pax). The classifier returns 0 inflight in that case:
let inflight = 0;
if (paxMax !== null) {
if (apiPaxeffectiveWcRemaining === 01 &&→ paxBooked > 0) {
// Likely stop-sold / cache lag - cannot infer inflight
inflight = 0;
} else {
inflight = Math.max(pax - apiPax, 0);
}
}wheelchairLimited[]
Dynamic
message
Output format
For ops to paste into Queue-it:
SOLD: 26/6, 27/6
LIMITED: 5/7
WC: 26/6, 27/6Empty lines are omitted; if nothing is sold/limited the field is an empty string.
12.Public Respondclassification JSONarrays
|
{{ JSON.stringify($json) }}Content-Type: application/json; charset=utf-8Cache-Control: public, max-age=15Response contract
The endpoint returns a single JSON object. Top-level shape:
{
"sold": ["27/6"],
"limited": ["5/7"],
"stopSold": [],
"detail": { "26/6": 256, "27/6": 0, "...": 0 },
"wheelchairSold": ["27/6"],
"wheelchairLimited": [],
"wheelchairDetail": { "26/6": 3, "27/6": 0 },
"dynamicMessage": "SOLD: 27/6\nWC: 27/6",
"updatedAt": "2026-05-26T22:07:45.148Z",
"source": "live",
"monitoring": { "...": "see below" }
}Top-level fields
sold"D/M"isAvailable=falselimitedstopSoldapiBlockedstring[]Dates isAvailable=falseapiPax=0 availabilityStringmonitoring detailobjectwheelchairSoldwheelchairLimitedwheelchairDetailobjectdynamicMessagestringupdatedAtISO source"live""cache"monitoringMonitoringdynamicMessage blockformat
SOLD: 26/6, 4/7
LIMITED: 11/7
WC: 26/6
Lines are only included if the respective array is non-empty. The message is blank if all dates are available. Queue-it reads this field to populate availability messaging on the waiting room page.
monitoring object
The monitoring object is not consumed by Queue-it but is available for ops dashboards and troubleshooting.
monitoring.season (season-level aggregates)
monitoring.byDate
Per-date object keyed by token (e.g. "26/6") with full classification detail:
{
"season": {
"totalCapacity": 6144,
"totalConfirmedPax": 73,
"totalConfirmedWc": 0,
"totalConfirmedBookings": 23,
"totalManifestTotalPax": 69,
"totalInflightPax": 0,
"percentSold": 1.19,
"datesTotal": 24,
"datesWithBookings": 11,
"datesNotRunning": 0,
"datesSold": 0,
"datesLimited": 0,
"datesStopSold": 0,
"datesWcSold": 0,
"datesWcLimited": 0,
"datesManifestMismatch": 1
},
"byDate": {
"27/26/6": {
"max": 256,
"confirmed": 16,2, // paid pax
"wcConfirmed": 0,
"bookings"pending": 5,4, // in-cart pax
"wcPending": 0,
"bookingsConfirmed": 1,
"bookingsPending": 1,
"inflight": 0,4, // alias of pending (from SQL statusType=6)
"remaining"confirmedRemaining": 240,254, // classification source
"apiPax": 240,250, // live checkout figure
"apiBlocked": false,
"effectiveCap": null, // operator cap if detected
"boundBy": "confirmed", // "capacity" | "confirmed" | "cap"
"manifestTotalPax": 12,237,
"manifestTotalAvailable": 244,19,
"manifestDelta": 4,-231, // negative = Manifest counter ahead of Txn (cancelled bookings not decremented)
"percentSold": 6.25,0.78,
"isAvailable": true,
"availabilityString": "plenty"
}
},
"topInflight": [],
"topConfirmed": [
{ "date": "27/6", "confirmed": 16, "bookings": 5 },
{ "date": "11/7", "confirmed": 11, "bookings": 5 }
],
"manifestMismatches": [
{ "date": "27/6", "txnConfirmed": 16, "manifestCounter": 12, "delta": 4 }
],
"health": {
"sqlRowCount": 24,
"sqlExpected": 24,
"sqlOk": true,
"apiResponseCount": 3,
"cacheState": "live",
"executionDurationMs": 5890,
"wcCap": 3,
"limitedThreshold": 20,
"confirmedFilter": "statusType=0"
}
}
On
monitoring.topInflight cached/ responses, monitoring.health.cacheState is "cache" and a cacheAgeSeconds field is added.
Verified production sample
topConfirmed
ExecutionTop-5 1254,dates capturedsorted 2026-05-26by 22:07:39Z.inflight Execution(pending) durationpax 5,890 ms cold, 24 SQL rows returned, 3 API chunk responses, source live.
Season totals from execution 1254
Topor confirmed (executionpax 1254)
respectively. Manifest counter mismatch (execution 1254)
monitoring.manifestMismatches
OnlyDates one date inwhere the 2026 season currently shows a discrepancy between Manifest.totalPax and the authoritative Txn-derived sum (confirmed pax:+ pending) differs from the Manifest counter. Reported for visibility of the CustomLinc counter drift bug.
monitoring.capped
CauseDates notwhere yetan identified.operator-set Thesales 11/12cap otherwas activedetected (effectiveCap is non-null), sorted by cap size ascending.
monitoring.apiBlocked
Detailed array of dates showwhere manifest_totalPax = pax_booked exactly. Thischeckout is documentedblocked furtherby underpending "Knownbookings anomalies"(not below.truly sold). Each entry includes: confirmed, pending, confirmedRemaining.
Cache behaviour
monitoring.health
| Description | ||
|---|---|---|
Rows |
source: "cache"monitoring.health.cacheAgeSecondsCache-Control: public, max-age=15Caveat: staticData does not survive an n8n process restart. After a restart the next call rebuilds fromby SQL + API.
Classification rules summary
availabilityStringnodeparturenoservicedatesNotRunningisAvailablesqlExpectedExpected row count (24 for full season)
sqlOktrue if sqlRowCount === soldstopSoldavailabilityStringremaining === 0apiResponseCountsoldNumber of CustomLinc API chunk responses received
0 < remaining ≤ 20executionDurationMslimitedClassify node execution time in ms
wc_remaining === 0limitedThresholdwheelchairSoldLimited threshold constant (20)
wc_remaining === 1confirmedFilterwheelchairLimitedstatusType IN (0,6) — SQL filter in use
classificationSourceconfirmedRemaining
detailSourceapiPax
inflightSourcestatusType=6
GrafanaDesign dashboardDecisions & Rationale
Why pending-blind classification?
APending companionbookings Grafana(statusType=6) dashboardrepresent readsseats directlyin active checkout carts. They auto-expire after a timeout if payment is not completed. Classifying a date as SOLD because of pending bookings would:
The apiBlocked flag is provided for ops monitoring so the team is aware when this condition exists, without affecting public availability messaging.
Why apiPax in detail[] rather than confirmedRemaining?
The detail[token] value is the live checkout figure that Queue-it can pass to a checkout integration. It reflects what the next customer can actually book, accounting for confirmed + pending + operator cap. Using confirmedRemaining here would overstate available seats (it ignores in-cart bookings).
Why suppress effectiveCap when stop-sold?
When a date is stop-sold (!isAvail), apiPax is forced to 0 by operator action. The residual cap calculation would produce a false cap equal to confirmed pax. Suppressing effectiveCap in this case avoids confusing monitoring output.
Manifest counter not used for classification
The Manifest.totalPax counter is unreliable for classification because cancelled bookings do not decrement it. On 2026-06-26 the counter shows 237 (from earlier test bookings) while the actual Txn sum is 2 confirmed + 4 pending. The endpoint uses direct Txn aggregation (statusType 0 and 6) as the authoritative source. Manifest data is retained in monitoring for counter drift visibility.
SQL Manifest-join pattern
The SQL query uses Manifest as the primary table (not Booking), joined to Txn/Booking via LEFT JOINs. This ensures all 24 season dates appear in the result even if they have zero bookings. An earlier version grouped by b.dateTimeOfTravel which collapsed booking-less Manifest rows into NULLs.
Operator cap detection
CustomLinc allows operators to set a per-departure sales cap via the UI. The cap column was not found in any accessible schema table (Manifest, ManifestAllocation, Movement, ProductOption all checked). The cap is inferred from the CustomLincresidual: MSSQLif datasourceapiPax < (itmax does- notconfirmed scrape- thispending), endpoint)the cap must equal (apiPax + confirmed + pending). SameThis sourceis a forward-compatible inference that works regardless of truth,where twothe consumers serving different jobs.
tol-onsale-2026customlinc-mssqlcf2gv2a7ki328a$season_start2026-06-26$season_end2026-07-20Panel inventory
The dashboard JSONcap is held by Mitch and can be re-imported via Grafana → Dashboards → New → Import.stored.
Known anomalies and ops notesIssues
27/6Silent Manifestfailure counter delta (currently 4)mode
As of 2026-05-26, Manifest.totalPax for 27 June reports 12 whileIf the Txn-derivedCustomLinc confirmedavailability paxAPI sumreturns isa 16.response Thewith othernull/empty 11 active TOL dates show exact agreement betweenproductAvailability, the twoworkflow counters.still Rootreturns causeHTTP not200 yetwith identified.sqlOk: Thetrueendpointbut surfacesno thisdate data in monitoring.manifestMismatches and monitoring.season.datesManifestMismatch. Action: investigate when convenient. Not blocking.
27/6 Manifest.max override didn't land
An operator attempt to set Manifest.max to 10 on 27/6 (to simulate a sold-out scenario for testing) did not propagate to the payload. Manifest.maxcolumnQueue-it —would thereceive DBan stillempty showsavailability 256.structure. WhetherThis CustomLincedge has a separate sales-cap mechanismcase is not yet known.handled with an explicit error response. Action:Status: confirmopen.
Manifest counter drift (CustomLinc bug)
Cancelling a booking does not decrement the Manifest.totalPax counter. This causes growing divergence between the counter and actual bookings on dates with cancellation activity. Does not affect classification (Txn is used directly). Reported to CustomLinc UIsupport. beforeStatus: relyingvendor onbug, operator-setmonitoring capacity overrides.only.
Pending booking expiry timing
Pending bookings (statusType=6) auto-expire on a CustomLinc internal timeout. The expiry duration is not yetdocumented. observableDuring inhigh-demand productiononsales, datamany
Percarts Mitchmay (PBRbe IT):open CustomLinc'ssimultaneously, pending/in-cartcausing bookingapiBlocked states exist but auto-cancel via a short timeout workflow, so they rarely persist long enoughdates to snapshot.appear Thetransiently. endpoint'sThese inflightresolve calculationautomatically inferswhen pendingcarts paxexpire fromwithout the difference between Txn-derived remaining and API live pax. Action: re-test inflight figures during a live onsale period to validate.
API stop-sold ambiguity
An earlier execution observed apiPax = 0 with isAvailable = true and availabilityString = "none" on 27/6, despite 10 confirmed bookings. This caused the previous (pre-guard) inflight calculation to report a misleading 246. The inflight guard in the Classify node prevents this. Action: if this pattern recurs during onsale, investigate whether CustomLinc has a per-departure stop-sold flag distinct from isAvailable.payment.
SecuritySeasonal and credentialsMaintenance
agentAPIKeyAt the start customlinc-mssqlGrafanaof datasourceeach (UID cf2gv2a7ki328a).
Maintenance notes
Annual rollover for nextnew TOL season
season, Thethe seasonfollowing window is hardcoded in two places. Bothconstants must be updated each year:
'2026-06-26''2026-07-20'datesSQL bookingsSEASON_START SEASON_END '2026-07-20''2026-07-19'The Classify node's
SQL_EXPECTED_ROWS Adjustingin thresholds
the LIMITED_THRESHOLDhealth =check)
20WC_CAP = 3WC_LIMITED_THRESHOLD = 1ttlSeconds = 30Cancelled bookings policy
TheAlso current SQL excludes cancelled bookings entirely (filter is statusType = 0, which does not include cancelled types). The Grafana dashboard has a separate "Cancelled bookings (last 50)" panel for visibility, but this endpoint deliberately does not surface them.verify:
Related artifacts
n8nCustomLincworkflowAutheditor:credentialshttps://n8n.pbr.org.au/workflow/T76MXW3QRGcoIBboare still valid (Login node)StatusTheprobeSQLworkflowsubscriber ID (discovery22)/hasreconciliationnottool):https://n8n.pbr.org.au/workflow/g42iOXUBX8QIkrzjchangedLiveTheendpoint:producthttps://n8n.pbr.org.au/webhook/availability/tolcode (TOL) matches the new season's productGrafanaQueue-itdashboarddynamicUID:messageformat matches the new season's campaign configurationtol-onsale-2026