Delay Comms — Stage 2: the delay call request
When an item is late for the second or a further time, this automation raises a call request for Customer Service instead of sending the customer another message. Once a day it reads the delayed lines out of NetSuite, opens one request per order in Kustomer with the delayed items listed on it, and routes it to the trade team or the account management team. It then stamps those order lines as Stage 2 so the order's customer-facing message reads "Delayed" and the request is not raised again for the same slip. Nothing in it contacts the customer — a person makes the call.
Rules & IDs
| Rule ID | Rule | Reading |
|---|---|---|
LI-BL-CMS-005 |
Which delays are handled by a call | Business logic |
LI-BL-CMS-006 |
One request per order, every late item on it | Business logic |
LI-BL-CMS-007 |
Who the call lands with | Business logic |
LI-BL-CMS-008 |
Recording the call, and not asking twice | Business logic |
LI-BL-CMS-009 |
What stops, and what carries on | Business logic |
| What | Name in the system | |
|---|---|---|
| Candidate feed search — unconfirmed, see DN-Q1NetSuite saved search | customsearch3826 — SCRIPT | DELAY | STAGE 2 | TASK | OVERSEAS | ITEM DELAY NOTIFICATION *legacy | open search 3826 |
| Reporting — Stage 2 summaryNetSuite saved search | customsearch5320 — ANALYSIS | DELAY COMMS | OPEN SALES ORDERS | STAGE 2 TASKS TO CALL | SUMMARY | open search 5320 |
| GitHub repository | lifeinteriors/kustomer-delay-comms @ 119f006 — kustomer-delay-comms | open at 119f006 |
The NetSuite links use the account-neutral host, which redirects a signed-in user to this account. Ids and names are exact, so a search stays findable by name even if a link does not resolve.
Three readings of the same rules. Nothing is duplicated between them. Printing gives you the business reading followed by the diagrams.
What it does. Once a day it looks for order lines that are running late again, and for each order it raises a single call request for Customer Service, listing every late item on that order with what the customer was originally promised, what is now expected, what they were last told, and how many days late that is. It routes the request to whichever team owns that customer, and then records on each late line that a call has been scheduled. It does not write to the customer.
Why it exists. The first time an item slips the customer gets a message. The second time, a message is not enough — somebody has to ring them, because by then the conversation is about whether they still want the order. Doing that by hand means somebody reading a report and deciding who to call, which is the step that quietly stops happening in a busy week. When it goes wrong the cost is in two directions: a customer who is never called finds out their furniture is months late by noticing, and cancels; and a customer who is called twice about the same slip is told the company does not know what it has already said to them. The recording step matters as much as the call, because it is what makes the order's own status line read "Delayed" to everyone who looks at it afterwards.
Who it affects. Customer Service feels it first — the requests land in their queue and they make the calls. Trade customers are handled by a different team from retail customers, so both notice if the routing is wrong. The customer is affected but never contacted by the automation itself. Supply chain and anyone reading an order's status sees the consequence, because recording the call is what changes the message on the order line.
When it runs. Once a day on a schedule, at about half past one in the afternoon Sydney time — though the schedule itself sits outside the automation and has been read off its output rather than confirmed. A line becomes eligible through a saved search in NetSuite whose conditions have not been read, which is the largest gap in this entry: the threshold for "late enough", whether it is limited to overseas supply, and whether drop-ship lines are excluded are all properties of that search. What is established is that an order can come back roughly a month later when its expected date slips again.
How it decides.
- It takes the late lines as a list and groups them by the order they belong to, so an order with four late items is one call request and not four.
- Within an order it keeps one entry per item name, so the same item appearing twice does not appear twice on the request.
- It finds or creates the customer in the service desk from the order's customer details, and if it cannot, it abandons that order and moves on to the next.
- It reads that customer back to see whether they are a trade customer, and sends the request to the trade team if so and to the account management team otherwise.
- It opens the request, attaches the list of late items to it, writes a note onto the sales order saying the request was raised, and stamps each late line as "call scheduled" together with the date the customer is now being told.
- Every step is conditional on the one before it except the last: the stamp goes on even when the note onto the order failed.
Outcomes.
| Outcome | What the customer gets | What the order gets |
|---|---|---|
| Request raised, everything recorded | Nothing yet — a person calls them | A note, and each late line stamped Stage 2 with the new date |
| Request raised, note onto the order failed | Nothing yet — a person calls them | Lines stamped Stage 2, but nothing on the order says why |
| Customer could not be found in the service desk | Nothing | Nothing. The order is skipped and named in the error email |
| Request could not be opened, or the item list could not be attached | Nothing | Nothing. The order is skipped and named in the error email |
| A row in the feed could not be read | Nothing, for anybody | Nothing, for anybody. The whole day's run is dropped and an error email is sent |
| Nothing was late | Nothing | Nothing, and no email at all |
Which delays are handled by a call LI-BL-CMS-005
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | A line appears in the delay feed | It is a candidate for a call request | The feed is the whole eligibility test. Nothing in the automation adds a condition of its own |
| 2 | The feed returns nothing | The run ends, and no email is sent at all | A quiet day and a broken feed look identical from outside. See DN-Q1 |
| 3 | A row in the feed is missing a value the automation reads | Every order in the run is dropped, and an error email is sent | Deployed behaviour, not intent — defect DN-D4 |
| 4 | The customer has not opted into marketing | The request is raised anyway | A call about a late order is service, not marketing |
| 5 | The customer cannot be found or created in the service desk | That order is abandoned and named in the error email | There is nowhere to raise the request against |
One request per order, every late item on it LI-BL-CMS-006
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | Several late lines belong to one order | They become one request, listing each item | One phone call covers the order. Four requests would mean four calls to the same person |
| 2 | Two rows name the same item, differing only in capitals | The item is listed once | The same product against two lines is one thing to talk about |
| 3 | Two rows name genuinely different items | Both are listed, numbered, in the order the feed returned them | The agent needs the whole picture before dialling |
| 4 | An item is listed | It carries what was promised, what is now expected, what the customer was last told, and the days late | Those four dates are what the conversation is about |
| 5 | The request is named | It is named for the order, as "Stage 2: Delay Call Request" and the order number | It is how the agent ties the request back to the order |
Who the call lands with LI-BL-CMS-007
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The service desk holds the customer as trade | The request goes to the trade team | Trade customers have a different relationship and a different team holds it |
| 2 | The service desk holds any other customer type | The request goes to the account management team | It is the default owner of a retail delay conversation |
| 3 | The service desk holds no customer type at all | The request goes to the account management team | The default is applied rather than the order being held |
| 4 | The customer cannot be read back from the service desk | The request goes to the account management team, and the run reports success | Deployed behaviour, not intent — defect DN-D5. A trade customer is silently misrouted |
| 5 | The order's own customer type in the ERP says trade | It makes no difference to routing | Routing reads the service desk's view of the customer, never the order's |
Recording the call, and not asking twice LI-BL-CMS-008
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The request and its item list were both created | Each late line is stamped "call scheduled", and the date the customer is now being told is written onto it | The stamp is what makes the order read "Delayed" to everyone else, and the date is the record of what was said |
| 2 | The same line slips again later | It can come back for a new request | The stored date is what a later run compares against, so a further slip is a new conversation |
| 3 | The line's identifier matches no line on the order | Nothing is stamped, and the run still reports the lines as updated | Deployed behaviour, not intent — defect DN-D1 |
| 4 | The write into the ERP fails | The run still reports it as updated, and the line keeps its old stage | Deployed behaviour, not intent — defect DN-D1. The order is eligible again on the next run |
| 5 | The automation runs twice in one day over the same line | Two requests are raised | Nothing checks for an existing request. The only guard is the stamp, and it has not landed yet — defect DN-D6 |
What stops, and what carries on LI-BL-CMS-009
| # | If… | Then… | Because |
|---|---|---|---|
| 1 | The customer could not be resolved | The order stops there — no request, no note, no stamp | There is no customer to raise it against |
| 2 | The request could not be opened | The order stops there — no note, no stamp | A note and a stamp with no request would tell Customer Service nothing |
| 3 | The item list could not be attached | The order stops there — no note, no stamp | A request with no items is not actionable |
| 4 | The note onto the sales order failed | The run carries on and stamps the lines anyway | Deployed behaviour, not intent — defect DN-D3. The order looks as though nothing was raised |
| 5 | Any order in the run failed at any step | An error email is sent, and no success email | The two emails are exclusive: one failure anywhere replaces the whole summary |
| 6 | Every order succeeded | A success email lists each order and each step against it | It is the only record of what the run did, outside the systems themselves |
Field registry
Every field this automation reads or writes, by the name it carries in the system — what it means, and what changes when it changes. Field ids are in the developer reading.
Inputs — what it reads
Values the automation looks at to make its decision. Change one of these and the decision changes.
| Field | What it means | What it changes | Status |
|---|---|---|---|
| Internal IDSales Order | The sales order the delayed lines sit on. | The grouping key. Every row carrying the same value becomes one call request, so a wrong value here splits or merges requests. | confirmed |
| Order NumberSales Order | The order number a person quotes — the NetSuite document number, or the Shopify number for web orders. | Printed into the conversation name, the note and the audit log. It is the only order identifier the agent making the call sees. | confirmed |
| Line IDSales Order line | Which line of the order is delayed. | The only thing the line-item write matches on. A line id that does not match any line on the order is skipped silently, and that line is never stamped. | confirmed |
| ProductSales Order line | The item name as the customer would recognise it. | Deduplicates the items on a request — two rows with the same name, differing only in case, become one item. Also the item list in the note and the audit log. | confirmed |
| Expected Ship DateSales Order line | When the line is currently expected to ship. | Printed in the note, and written back as the new Delay Ship Date. It is therefore both what the agent is told and what suppresses the next request. | confirmed |
| Delay Ship DateSales Order line | The ship date last communicated to the customer about this line. | Read as "last delay comm date" for the note, then overwritten with the current Expected Ship Date. The gap between the two is what a later run can compare, so this field is the brake on the request repeating. | confirmed |
| Original Lead TimeSales Order line | The ship date the customer was promised when they ordered. A date, not a number of days, despite the label. | Printed in the note as the baseline the agent quotes. Written by LI-BL-ORD-001. | confirmed |
| Days DelayedSales Order line | How many days later than promised the line is now expected. | Printed in the note. Whether it also gates eligibility is a property of the feed search, which has not been read — see DN-Q3. | confirmed |
| Customer Internal IDCustomer | The NetSuite customer behind the order. | The key the customer-resolution service matches a Kustomer customer on. Without a resolved customer the request is abandoned. | confirmed |
| Customer TypeCustomer | What kind of customer this is — retail, trade, and so on. | Passed to the customer-resolution service. Note that team routing does NOT read this field; it reads the type held in Kustomer instead. | confirmed |
| needs confirmingKustomer customer | needs confirming | The single field that decides which team the call lands with. The exact value `trade` routes to Trade Enquiries; anything else, including a missing value or a failed read, routes to Account Management. | needs confirming |
| Email AddressCustomer | The customer's email address. | Passed to the customer-resolution service, which uses it to find or create the Kustomer customer. | confirmed |
| Customer PhoneCustomer | The number the call will be made on. | Passed to the customer-resolution service and held on the Kustomer customer. This is a call request, so a wrong number makes the whole request undeliverable. | confirmed |
| Customer First NameCustomer | The customer's first name. | Passed to the customer-resolution service. | confirmed |
| Customer Last NameCustomer | The customer's last name. | Passed to the customer-resolution service. | confirmed |
| Name1Customer | needs confirming | Sent as the customer-resolution service's entity id. Its label is read, not its value, so a renamed entity changes what is sent. | needs confirming |
| Customer Company NameCustomer | The company on the customer record, where there is one. | Passed to the customer-resolution service. The literal text "none", in any punctuation or casing, is treated as empty rather than as a company called None. | confirmed |
| Customer Fax IDCustomer | needs confirming | Passed to the customer-resolution service, with the same "none" means empty handling as the company name. What this field is actually used for is not established — see DN-Q5. | needs confirming |
| Customer Initial Contact ChannelCustomer | needs confirming | Passed to the customer-resolution service as a label. | needs confirming |
| Customer Shopify StoreCustomer | needs confirming | Passed to the customer-resolution service as a single-element list of the label. | needs confirming |
| Customer Application ApprovalCustomer | needs confirming | Passed to the customer-resolution service as the trade application approval, with "none" treated as empty. Not used for team routing. | needs confirming |
| Customer Accepts MarketingCustomer | Whether the customer has opted into marketing. | Passed to the customer-resolution service. It does not gate this automation — a delay call request is raised regardless, because it is a service contact rather than marketing. | confirmed |
| Customer Address Line 1 / Line 2 / City / State / ZipCustomer | The customer's address, sent as one default shipping address. | Passed to the customer-resolution service. The country is always sent as AU and the suburb always blank, whatever the order says; both are hard-coded. | confirmed |
Outputs — what it writes
Values the automation puts back. Everything downstream of this rule reads them, so a wrong value here travels.
| Field | What it means | What it changes | Status |
|---|---|---|---|
| Delay Ship DateSales Order line | The ship date last communicated to the customer about this line. | Read as "last delay comm date" for the note, then overwritten with the current Expected Ship Date. The gap between the two is what a later run can compare, so this field is the brake on the request repeating. | confirmed |
| Delay CommsSales Order line | Which stage of delay communication has happened on this line. Set to value 2, Stage 2: Delay Call Scheduled. | Read by the Item Stage automation to decide whether the customer-facing message says "Delayed", and by the delay reporting. Writing it is what records that the call was raised. | confirmed |
This entry is draft. The code path is verified at commit 119f006 and the behaviour is
corroborated against production data, but the automation's inputs are configured through Lambda
environment variables that are not in the repository — above all N_SEARCHID, the saved search that
decides which lines are eligible. Everything about eligibility is therefore unread. Claims below are
marked verified (read at a named file and line, or from a production query), inferred, or
unknown.
The deployed shape is one Lambda, kustomer-delay-comms, plus one NetSuite RESTlet that ships in the
same repository under NetSuite Scripts/. Two further RESTlets — the saved search extractor and the
user note creator — are called by script id through the NS-Queuer API Gateway and live elsewhere.
Inputs
What it reads, and where from.
| Field / source | System | Type | Notes |
|---|---|---|---|
| Saved search rows, via the extractor RESTlet through NS-Queuer | NetSuite | JSON string | Verified controllers/netsuite.mjs:122-147. The response arrives as a JSON string and is parsed; the first element is a header row and is dropped |
N_SEARCHID |
Lambda environment | string | Unknown. The search that decides eligibility. Candidate customsearch3826 — DN-Q1 |
| Order grouping and item columns — Internal ID, Order Number, Line ID, Product, Expected Ship Date, Delay Ship Date, Original Lead Time, Days Delayed | NetSuite saved search | mixed | Verified controllers/formatter.mjs:14-27, 53-63. Internal ID and Customer Internal ID are read as [0].value; Name1, Customer Initial Contact Channel, Customer Shopify Store, Customer Application Approval and Customer Type as [0].text |
| Customer columns — email, phone, names, company, fax id, contact channel, Shopify store, application approval, type, accepts marketing, address | NetSuite saved search | mixed | Verified controllers/formatter.mjs:66-118. Passed as one payload to the customer-resolution service |
| Kustomer customer, read back by id | Kustomer | JSON | Verified controllers/kustomer.mjs:9-37. Read only to resolve the team |
The customer's type, at attributes.custom.typeTree |
Kustomer | string | Verified controllers/kustomer.mjs:44-54. Compared with the exact string trade |
KT_TEAM_TRADE, KT_TEAM_ACCOUNT_MGMT or KT_TEAM |
Lambda environment | string | Verified in production as Kustomer team ids: 625f40eadf9fff212e388b3c is Trade Enquiries, 6274a4ec75a36715c102a5a2 is CS - Acc Management, display name Account Management. Both observed on Stage 2 conversations |
KT_REASON |
Lambda environment | string | Verified in production as Campaigns::Stage 2 Delays, read from conversation 6ab348065093dee73eed3090 |
KT_QUEUE |
Lambda environment | string | Unknown — DN-Q6. The code sends it; the conversation read back carries no queue |
N_NOTETYPE, N_NOTEDIR |
Lambda environment | integer | Unknown — DN-Q5. Cast to numbers and attached to the user note |
Hard-coded rather than read: the address country is always AU, the suburb always empty, and the
address is always flagged as the default shipping address — verified
controllers/formatter.mjs:109-114, where the comment records that the default was agreed with the
team.
Processing
What it does with that, step by step.
once a day, on a schedule that is not declared here:
ask NetSuite for the delay feed
feed empty or unreadable -> stop, send nothing
drop the header row
group the rows:
for each row
first time this order is seen -> start a request for it, carrying the customer payload
item name already on it -> skip the row
otherwise -> add the item, with its four dates and its line id
any row that cannot be read -> abandon the whole batch, report an error
for each request, in order:
1 resolve the customer in the service desk -> no customer, abandon this order
2 read that customer back, look at its type -> trade, route to the trade team
anything else or unreadable, account management
3 open the request -> not opened, abandon this order
4 build the item list -> not built, abandon this order
5 attach the item list -> not attached, abandon this order
6 write a note onto the sales order -> failed, record it and CARRY ON
7 stamp each late line: stage 2, and the date now being communicated
any failure anywhere -> one error email, no summary
no failures -> one summary email listing every order and every step
The stamp at step 7 is applied by loading the sales order, walking its lines, matching each fed line
id against the line, and writing both fields before committing — verified
NetSuite Scripts/delayCommsLineItemUpdates.js:59-154. The date written into Delay Ship Date is the
Expected Ship Date that was read, not a new calculation — verified
controllers/netsuite.mjs:165-175.
What suppresses the next request
Nothing in the Lambda holds state between runs, and nothing checks Kustomer for an existing request
— verified by reading controllers/kustomer.mjs:169-284 in full. The suppression is entirely a
property of the two fields written at step 7 and of the unread feed search that reads them.
Production supports the mechanism being the Delay Ship Date snapshot rather than the Delay Comms
flag: order S306188 received a Stage 2 request on 2026-06-25, 2026-07-23, 2026-08-19 and 2026-09-15,
roughly four weeks apart, which is a line already stamped Stage 2 re-qualifying each time its
expected date moved again. Inferred, because the search's conditions are unread — DN-Q1, DN-Q3.
Decision tree
The same logic as a path through the decisions, in the order they run.
the daily delay feed
├─ feed empty ? ──► stop. no email at all, so a broken feed
│ looks exactly like a quiet day LI-BL-CMS-005 T?
├─ a row unreadable ? ──► abandon EVERY order in the run, error email DN-D4 T10
└─ rows present
│ group by order first, THEN dedupe items within it — the grouping
│ is what makes one phone call per order rather than per line
├─ order already grouped ? ──► add this item to it LI-BL-CMS-006 T1
└─ new order ──► start a request LI-BL-CMS-006 T2
│
│ from here the steps are strictly ordered: each needs the id
│ the previous one produced, so none can be reordered
├─ 1. customer resolved in Kustomer ?
│ └─ no ──► abandon this order, error email. nothing written LI-BL-CMS-009 T6
├─ 2. read the customer back, look at its type
│ ├─ exactly "trade" ──► Trade Enquiries LI-BL-CMS-007 T3
│ ├─ anything else / absent ──► Account Management LI-BL-CMS-007 T4
│ └─ read FAILED ──► Account Management, silently DN-D5 T5
│ the fallback cannot tell a retail customer from an
│ unreadable one, so a trade order is misrouted
├─ 3. conversation opened ?
│ └─ no ──► abandon this order. no note, no stamp LI-BL-CMS-009
├─ 4/5. item list built and attached ?
│ └─ no ──► abandon this order. no note, no stamp LI-BL-CMS-009
│ note body carries a literal \(s\) on its first line DN-D2
├─ 6. user note onto the sales order
│ ├─ created ──► carry on LI-BL-CMS-009
│ └─ FAILED ──► carry on ANYWAY. this is the one step that
│ does not gate the next, so the order can be
│ stamped as called with nothing on it
│ explaining why DN-D3 T9
└─ 7. stamp each late line in NetSuite
├─ line id matches a line ──► Delay Comms = 2, and
│ Delay Ship Date = the
│ Expected Ship Date read LI-BL-CMS-008 T7
├─ line id matches nothing ──► nothing stamped, reported
│ as updated DN-D1 T8
└─ RESTlet errored ──► returns its error as HTTP 200,
so the Lambda reads it as
success. the line keeps its
old stage and the order is
eligible again tomorrow DN-D1 T8
The ordering that matters and cannot be rearranged: the customer must be resolved before the conversation, because the conversation is created against the customer id; the conversation must exist before the note, because the note carries the conversation id; and the stamp must come last, because it is the thing that takes the order out of the feed. The one ordering that is not enforced is the audit note before the stamp — which is exactly what DN-D3 is.
Outputs & side effects
What it writes, and who reads it afterwards.
| Output | Written to | Downstream consumer |
|---|---|---|
Conversation, named Stage 2: Delay Call Request - <order number>, status open, priority 3, reason Campaigns::Stage 2 Delays, assigned to one team |
Kustomer | Customer Service, who make the call. Verified controllers/kustomer.mjs:57-77 and in production |
| Note on that conversation, listing each late item with its four dates | Kustomer | The agent making the call. Verified controllers/kustomer.mjs:79-103 and in production |
User note on the sales order, titled Delay Schedule Call Task Created |
NetSuite | The audit trail on the order. Verified controllers/netsuite.mjs:51-69 |
custcol_delay_comms = 2 on each late line |
NetSuite sales order line | The Item Stage automation's "Delayed" wording, and the delay reporting. Verified NetSuite Scripts/delayCommsLineItemUpdates.js:121-126 |
custcol_delay_ship_date on each late line |
NetSuite sales order line | Read back by this automation as "last communicated date", and by the feed search. Verified NetSuite Scripts/delayCommsLineItemUpdates.js:113-118 |
| Success email, or error email with a CloudWatch link | AWS SES | The developer group. Verified controllers/aws.mjs:52-167, index.mjs:25-43 |
Nothing is sent to the customer. Verified in production: conversation
6ab348065093dee73eed3090 carries messageCount: 0 and outboundMessageCount: 0, and no message
send appears anywhere in the code.
The sales order is saved with enableSourcing: true and ignoreMandatoryFields: true — verified
NetSuite Scripts/delayCommsLineItemUpdates.js:151-154. It is a full record load, edit and save of
the sales order, not a line-level patch, so any other user-event script on sales order save will fire.
Worked examples
Real records carried end to end, so the logic can be checked against something that happened.
Example 1 — the ordinary case, order S317808
Traced from the Kustomer note and conversation the automation created on 2026-09-23.
| Step | Value | Why |
|---|---|---|
| Input | One late line on S317808 · Everyday Comfort Mattress (Firm, King) · original lead time 7/9/2026 · expected ship date 9/10/2026 · last delay comm date 22/9/2026 · days delayed 32 | The line slipped again after the customer was last told 22/9 |
| Grouping | One order, one item | LI-BL-CMS-006 |
| Routing | Customer's Kustomer type is not trade → team 6274a4ec75a36715c102a5a2, Account Management |
LI-BL-CMS-007 row 2 |
| Request | Conversation 6ab348065093dee73eed3090, named Stage 2: Delay Call Request - S317808, open, priority 3, reason Campaigns::Stage 2 Delays |
LI-BL-CMS-006 row 5 |
| Note | Note 6ab3480762417a735f6cce06, one item with all four dates — and a literal \(s\) on its first line |
DN-D2 |
| Written | Each late line stamped Delay Comms = 2, Delay Ship Date = 9/10/2026 | LI-BL-CMS-008 row 1 |
| Afterwards | An agent picked it up, noted "Already cancelled for store credit", and closed it the same day | The call request did its job — the order was already resolved |
Note that the days delayed figure is the gap between the promised date and the currently expected one — 7/9/2026 to 9/10/2026 is 32 days. The arithmetic is verified against the note; that the feed search computes it this way is inferred.
Example 2 — the awkward case, the 2026-06-11 duplicates
| Step | Value | Why |
|---|---|---|
| Input | Orders S309007, S310078 and S300636, already carrying Stage 2 requests | All three were in the feed |
| What happened | S309007 and S310078 each received four Stage 2 conversations between 06:35 and 07:02 UTC; S300636 received three. All were assigned to Account Management | Nothing checks Kustomer for an existing request — DN-D6 |
| Contrast | Every other request in the sample was created between 03:30 and 03:40 UTC | These sit outside the daily window, so the run was started by hand |
| Written | Each run stamped the same lines again | The stamp is idempotent; the conversation is not |
| Outcome | Three customers were queued for three or four calls about one delay | Whether this was testing or an incident is DN-Q7 |
Example 3 — a Stage 2 line whose message is not "Delayed"
Order S309811, line 1, Aria Leather Dining Chair (Walnut, Chocolate Brown), read from production on
2026-09-28: custcol_delay_comms = 2, custcol_delay_ship_date = 25/9/2026, but
custcolcust_item_stage = Ready to be shipped. Stamping Stage 2 does not guarantee the customer
sees the word "Delayed" — the Item Stage rules put stock-on-hand wording first. See the conflicts
noted under Known limitations.
Test cases
What to run to know the behaviour is intact. Expected values, never “should work”.
| Case ID | Layer | Preconditions | Input | Expected output | Rule ref |
|---|---|---|---|---|---|
DN-TC-T1 |
Feed and formatting | The feed search returns three rows for one sales order — two different items, and one item repeated with different casing. | One run of the Lambda. | One call request is raised for the order, listing two items. The repeated item appears once. | LI-BL-CMS-006 |
DN-TC-T2 |
Feed and formatting | The feed search returns rows for four different sales orders. | One run of the Lambda. | Four separate call requests, one per order, each naming only its own items. | LI-BL-CMS-006 |
DN-TC-T3 |
Team routing | The order's Kustomer customer carries the type value trade. |
One run of the Lambda. | The conversation is assigned to Trade Enquiries. | LI-BL-CMS-007 |
DN-TC-T4 |
Team routing | The order's Kustomer customer carries no type value, or a value other than trade. |
One run of the Lambda. | The conversation is assigned to Account Management. | LI-BL-CMS-007 |
DN-TC-T5 |
Team routing | The order's Kustomer customer carries trade, but the read of that customer fails. |
One run of the Lambda. | Deployed behaviour is that the conversation is assigned to Account Management and the run reports success. This is defect DN-D5; the intended outcome is Trade Enquiries. | LI-BL-CMS-007 |
DN-TC-T6 |
Kustomer | The customer-resolution service returns no customer id for the order. | One run of the Lambda. | No conversation, no note, no audit log and no line-item write for that order. An error email names the order. Other orders in the same run are unaffected. | LI-BL-CMS-009 |
DN-TC-T7 |
NetSuite write | An order with one delayed line, Delay Comms blank or Stage 1, Expected Ship Date later than Delay Ship Date. | One run of the Lambda that completes every step. | That line's Delay Comms reads Stage 2: Delay Call Scheduled, and its Delay Ship Date now equals the Expected Ship Date that was read. | LI-BL-CMS-008 |
DN-TC-T8 |
NetSuite write | A row whose Line ID matches no line on the sales order — for example a line since removed. | One run of the Lambda. | No line is stamped and the order is saved unchanged, but the run reports the line items as updated. This is defect DN-D1. | LI-BL-CMS-008 |
DN-TC-T9 |
Ordering | An order that reaches the audit-log step, with the user note RESTlet failing. | One run of the Lambda. | Deployed behaviour is that the line items are still stamped Stage 2 and the order carries no user note. This is defect DN-D3. | LI-BL-CMS-009 |
DN-TC-T10 |
Feed and formatting | The feed search returns ten rows, one of which is missing the Product column. | One run of the Lambda. | Deployed behaviour is that no order at all is processed and an error email is sent. This is defect DN-D4. | LI-BL-CMS-005 |
DN-TC-T11 |
Idempotency | An order that already has a Stage 2 conversation raised earlier the same day, with its line still selected by the feed search. | A second run of the Lambda on the same day. | Deployed behaviour is that a second conversation and a second note are raised. This is defect DN-D6. | LI-BL-CMS-008 |
DN-TC-T12 |
Reporting | A run in which every order succeeds at every step. | One run of the Lambda. | One success email listing each order and each step against it. No error email. | LI-BL-CMS-009 |
These cases are derived from reading the code and from production queries, not from a passing
run. The repository has no automated tests — npm test is the default "no test specified" stub —
and the one script under scripts/ is a manual harness that creates a real conversation against a
hard-coded live customer id, so it must not be run casually. Five of the twelve cases below record
deployed behaviour that is a defect rather than the intended outcome; each says so and names the
defect.
| T1 | | | | LI-BL-CMS-006 |
UAT
What a person checks, by hand, before it is trusted.
| # | Step | What to check | Signed off by | Date |
|---|---|---|---|---|
| U1 | Open the Stage 2 summary saved search in NetSuite and pick an order listed on it | It names an order with at least one late line | ||
| U2 | In Kustomer, search conversations for Stage 2: Delay Call Request - <that order number> |
Exactly one open request exists for the current slip, assigned to Account Management or Trade Enquiries | ||
| U3 | Open that request and read its note | Every late item on the order is listed once, each with original lead time, expected ship date, last delay comm date and days delayed | ||
| U4 | Open the same sales order in NetSuite and read the Delay Comms column on those lines | Each reads Stage 2: Delay Call Scheduled | ||
| U5 | Read the Delay Ship Date column on the same lines | It matches the expected ship date quoted in the Kustomer note | ||
| U6 | Open the order's user notes | There is a note titled Delay Schedule Call Task Created naming the same items | ||
| U7 | For a known trade customer's delayed order, repeat U2 | The request is assigned to Trade Enquiries, not Account Management | ||
| U8 | Search Kustomer for more than one open Stage 2 request on the same order number | There should be none for the same slip. More than one is DN-D6 |
Edge cases
The inputs that sit at the boundary, and whether each is handled.
| Case | Behaviour | Handled? |
|---|---|---|
| Two feed rows for the same item on one order, differing only in capitals | Listed once. The comparison lowercases both names | ✅ |
| Two feed rows for the same line id | The line is stamped once. The write payload is deduplicated on line id | ✅ |
| Company name, address line 2, fax id or application approval holding the literal text "none" in any casing or punctuation | Treated as empty rather than as a value | ✅ |
| An order whose customer has no type in Kustomer | Routed to Account Management | ✅ |
| Feed returns nothing | Run ends silently, with no email of any kind | ⚠️ Handled, but indistinguishable from a broken feed |
| Feed row missing a column the formatter reads | The whole run is dropped | ❌ DN-D4 |
| Line id in the feed matching no line on the order | Silently skipped, and reported as updated | ❌ DN-D1 |
| Kustomer customer read fails for a trade customer | Misrouted to Account Management, reported as success | ❌ DN-D5 |
| User note RESTlet fails | Lines are stamped anyway | ❌ DN-D3 |
| The automation run twice in a day over the same line | Two requests raised | ❌ DN-D6 |
| An address outside Australia | Sent as AU regardless |
⚠️ Deliberate — AU-only business. See Known limitations |
| A line that catches up after being stamped Stage 2 | Not established. Nothing here resets the field | ❓ DN-Q4 |
Failure modes
What breaks it, how that shows, and how to recover.
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Feed search renamed, deleted or its id changed | No requests raised; no email at all | None automatic — silence is the symptom. The Stage 2 summary search growing with no matching Kustomer requests is the manual check | Restore the search id in the Lambda environment |
| NS-Queuer unavailable or rate-limiting | The extractor call throws and the run ends; error email | Error email with CloudWatch link. The NS-Queuer client retries rate-limit errors with backoff before giving up | Re-run once NS-Queuer is healthy |
| Kustomer API token expired | Every order fails at the conversation step; error email lists them all | Error email | Replace KT_BEARER; the orders are still in the feed, so the next run picks them up |
| Customer-resolution service down | Every order fails at the first step; error email | Error email | Fix the service; orders remain eligible |
| Line-item RESTlet failing | None. Reported as success, and the lines silently keep their old stage | Only visible as the same order reappearing in the feed day after day | Fix the RESTlet. DN-D1 is what hides this |
| User note RESTlet failing | Orders stamped Stage 2 with no note on them | Error email does name it, because the failure is recorded even though the run continues | Re-create the notes by hand. DN-D3 |
| SES send failing | No email either way; the run itself still completed | CloudWatch only | The work is done; only the report is lost |
| One malformed feed row | Nobody is called that day | Error email, but it does not say how many orders were dropped | Fix the row or the search, re-run. DN-D4 |
Known limitations
Scope that was deliberately chosen. A limitation is a decision.
| Limitation | Why it is this way | What to do instead |
|---|---|---|
| The automation contacts nobody. It only raises work for a person | A second delay is a conversation, not a notification. The decision of what to say belongs to an agent | Expect the customer to hear nothing until someone calls. Measure the queue, not the automation |
Country is always AU and suburb always blank |
The business sells only into Australia, and the default was agreed with the team | Nothing. It is correct for the market |
| Eligibility lives entirely in a saved search, not in code | It lets the business retune "late enough" without a deployment | Change the search, not the Lambda — but note that nothing in version control will record the change |
| Marketing consent is ignored | A delay call is service, not marketing | Nothing |
| Routing reads Kustomer's view of the customer, not the order's | Kustomer is where the team structure lives | To change routing, change the customer in Kustomer |
| Stamping Stage 2 does not guarantee the customer-facing message reads "Delayed" | The Item Stage rules put stock-on-hand wording ahead of the delay wording, so a line that has since arrived reads Ready to be shipped |
Read Delay Comms for "have we told them", and Item Stage for "what do they see". They answer different questions |
| The two emails are exclusive | One failure anywhere replaces the entire summary | On an error email, check the systems rather than expecting a partial summary |
| There are no automated tests | None were written | Use the cases above by hand. Do not run scripts/test-create-conversation.mjs against production without reading it first — it creates a real conversation against a hard-coded customer id |
Known defects
Where it does something other than what was decided. A defect is a mistake.
| Ref | Status | Defect | Rule |
|---|---|---|---|
DN-D1 |
Open | A failed line-item write is counted as a success. The NetSuite RESTlet catches its own errors and returns the error object as a normal HTTP 200 response, and the Lambda treats any 200 with a body as "updated". So when the write fails, Delay Comms is never set to 2, nothing says so, the success email reports the line items as updated, and the order stays in whatever state the feed search selects on. Verified by reading NetSuite Scripts/delayCommsLineItemUpdates.js:169-176 against controllers/netsuite.mjs:177-186. Not yet reproduced against production, so the frequency is unknown. |
LI-BL-CMS-008 |
DN-D2 |
Open | The note's first line renders a literal backslash before each bracket — it reads "for the following delayed item(s):" to the agent. The escaping is written into the note body at controllers/kustomer.mjs:84 and Kustomer stores and displays it as typed. Verified in production: the stored body of note 6ab3480762417a735f6cce06 on conversation 6ab348065093dee73eed3090 (order S317808, 2026-09-23) carries the backslashes. Cosmetic, but it is the first line of every delay call request an agent reads. |
LI-BL-CMS-006 |
DN-D3 |
Open | The line items are stamped even when the audit note failed. Every earlier step aborts the order on failure, but the audit-log step does not — controllers/kustomer.mjs:256-270 records the failure and falls straight through to the line-item write. The result is an order marked as called, with the customer-facing message switched to "Delayed", and no user note on the sales order saying who raised it or when. The sales order itself then looks as though the call was never requested. |
LI-BL-CMS-009 |
DN-D4 |
Open | One unreadable row drops the entire day's batch. The row formatter throws when a row is missing a field it indexes into, and the caller catches that and returns an empty list (controllers/formatter.mjs:33-37 and 140-144), so every other order in the run is discarded too. An error email is sent, so this is visible rather than silent — but nobody is called that day, and the email does not say how many orders were dropped. |
LI-BL-CMS-005 |
DN-D5 |
Open | A failed customer read silently routes a trade customer to the wrong team. Team routing reads the customer back from Kustomer, and when that read fails the code carries on with no customer rather than stopping — controllers/kustomer.mjs:201-202 with 39-55. Every fallback path, including a missing customer, returns Account Management, so a trade order lands with the retail team and looks correctly assigned. |
LI-BL-CMS-007 |
DN-D6 |
Open | Nothing in the automation checks whether a request already exists, so re-running it repeats the request. Verified in production: on 2026-06-11 orders S309007 and S310078 each received four Stage 2 conversations between 06:35 and 07:02 UTC, and S300636 received three, all outside the normal daily window. The only guard is the Delay Comms write in NetSuite, which does not help a run that starts before the previous one's write is visible to the feed. Whether that day was deliberate testing or a production incident is DN-Q7. | LI-BL-CMS-008 |
All six were established by reading the code at commit 119f006. DN-D2 and DN-D6 are additionally
confirmed against production data; the other four are confirmed in the code path but have not been
reproduced against a live run, so their frequency is unknown. None has been raised as a change
request, and nothing here has been changed in the source repository — this registry records
behaviour, it does not fix it.
Open questions
What could not be established. Recorded rather than guessed.
| Ref | Status | Question | Who can answer |
|---|---|---|---|
DN-Q1 |
Open | Which saved search feeds this automation? The search id is held in the Lambda's environment as N_SEARCHID and does not appear in the repository. The only Stage 2 script search in NetSuite is customsearch3826, "SCRIPT | DELAY | STAGE 2 | TASK | OVERSEAS | ITEM DELAY NOTIFICATION *legacy" — its name matches and it returns no rows today, but its *legacy marker sits oddly against a live automation, and its column set has not been matched against the columns the formatter reads. Until this is confirmed, every eligibility condition below is unread. | Christian Sy |
DN-Q2 |
Open | What invokes the Lambda, and on what schedule? Nothing in the repository declares a trigger. From the creation times of the conversations it raises, it appears to run once a day at about 13:30 Sydney time, which is also when the Stage 1 delay emails are timestamped — but that is read off the output, not off a schedule. | Julian Ayoub |
DN-Q3 |
Open | What actually makes a line eligible? Specifically: is there a Days Delayed threshold, is it restricted to overseas supply as the search name suggests, are drop-ship lines excluded, and does it require Delay Comms to already be Stage 1? All of these live in the feed search, not in the code. The code's own comment about excluding drop-ship lines does not match what it does, which suggests the exclusion was expected to be in the search. | Christian Sy |
DN-Q4 |
Open | What clears or ends a Stage 2 request? Nothing in this automation resets Delay Comms, and no other documented rule writes that field. So it is not established whether a line that catches up is ever taken back off Stage 2, or whether the value simply stays for the life of the order. | Phuong |
DN-Q5 |
Open | What are the note type and direction the audit log is created with, and what is the Customer Fax ID field really carrying? All three are environment values or unlabelled fields that the code passes through without using. | Christian Sy |
DN-Q6 |
Open | Is a Kustomer queue set on these conversations? The code sends a queue id from its environment, but the conversation read back from production shows no queue on the record. Either the value is unset or the queue is not returned by the API. | Julian Ayoub |
DN-Q7 |
Open | Was the cluster of duplicate requests on 2026-06-11 deliberate testing or a production incident? Three orders received three or four requests each within half an hour, outside the daily window. If it was testing, four customers were queued for calls they did not need. | Julian Ayoub |
DN-Q8 |
Open | What does the Stage 1 delay message actually say, and which channel is authoritative? Stage 2 assumes the customer has already been told something, but the wording lives outside this repository, and NetSuite carries two live Stage 1 script searches — one for SMS (customsearch5152) and one for Klaviyo (customsearch6155) — plus an inactive Dotdigital one. Nothing read here establishes whether both fire, or what either sends. | Phuong |
The largest is DN-Q1. Until the feed search is identified and read, this entry describes what the automation does with the lines it is given but not which lines those are — so the thresholds, the overseas restriction implied by the search names, and the drop-ship exclusion the code's own comment expects are all unread.
Source references (read-only)
The code and searches this reading was written from.
lifeinteriors/kustomer-delay-comms/index.mjs@119f006— Lambda handler, error/success email branch (verified 2026-09-28)lifeinteriors/kustomer-delay-comms/controllers/app.mjs@119f006— orchestration: feed, format, process (verified 2026-09-28)lifeinteriors/kustomer-delay-comms/controllers/formatter.mjs@119f006— grouping by order, item dedupe, customer payload, the "none" handling (verified 2026-09-28)lifeinteriors/kustomer-delay-comms/controllers/kustomer.mjs@119f006— team routing, conversation and note creation, per-order step ordering (verified 2026-09-28)lifeinteriors/kustomer-delay-comms/controllers/netsuite.mjs@119f006— saved search extraction, user note audit, line-item update payload (verified 2026-09-28)lifeinteriors/kustomer-delay-comms/controllers/aws.mjs@119f006— customer resolution call, SES success and error emails (verified 2026-09-28)lifeinteriors/kustomer-delay-comms/helpers/errors.mjs@119f006— error collection and CloudWatch link (verified 2026-09-28)lifeinteriors/kustomer-delay-comms/helpers/nsQueuer.mjs@119f006— NS-Queuer client, status polling and rate-limit backoff (verified 2026-09-28)lifeinteriors/kustomer-delay-comms/NetSuite Scripts/delayCommsLineItemUpdates.js@119f006— the line-item update RESTlet, including its error-as-200 return (verified 2026-09-28)- NetSuite ·
transactionLine— Delay Comms and Delay Ship Date populations, and Stage 2 lines by month (queried 2026-09-28) - NetSuite · saved search list matching "delay" — the Stage 1 and Stage 2 script searches (read 2026-09-28)
- Kustomer · conversations named
Stage 2: Delay Call Request— 440 matched; the first 60 analysed for duplicates, teams, statuses and creation times (queried 2026-09-28) - Kustomer · conversation
6ab348065093dee73eed3090and note6ab3480762417a735f6cce06— reason code, message counts, note body (read 2026-09-28) - Kustomer · team list — resolving the two assigned team ids to Trade Enquiries and CS - Acc Management (read 2026-09-28)
Change history
What moved on this page, and when.
| Date | Change | Change request |
|---|---|---|
| 2026-09-28 | Initial documentation of existing behaviour, written against commit 119f006 of lifeinteriors/kustomer-delay-comms. Five rules allocated in the CMS domain, LI-BL-CMS-005 to LI-BL-CMS-009. The code path is fully read; the eligibility feed is not, because it is configured through a Lambda environment variable — recorded as DN-Q1 and the reason this entry is draft. Six defects recorded, two of them confirmed against production data. Team ids, reason code and the absence of any customer-facing message were verified against Kustomer; the Delay Comms and Delay Ship Date writes were verified against NetSuite. Nothing in the source repository was modified. Three glossary terms added and referenced rather than left floating — LI-TERM-DELAY-CALL-REQUEST and LI-TERM-DELAY-SHIP-DATE with meanings derived from the verified behaviour, and LI-TERM-DELAY-STAGE-1 with none, because what Stage 1 sends could not be read. Five gaps recorded on MAP.md, and the gaps row that named a Celigo flow as the likeliest writer of Delay Comms was narrowed to Stage 1, since the Stage 2 write is now established. |
— |
Field registry — ids
Inputs
| Field id | Field name | Record | Source | Type | Read / written | Used by |
|---|---|---|---|---|---|---|
internal_id |
Internal ID | Sales Order | Saved search (id unconfirmed — see DN-Q1) | integer | read | Which delays are handled by a callLI-BL-CMS-005 One request per order, every late item on itLI-BL-CMS-006 |
order_number |
Order Number | Sales Order | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | One request per order, every late item on itLI-BL-CMS-006 |
line_id |
Line ID | Sales Order line | Saved search (id unconfirmed — see DN-Q1) | integer | read | Recording the call, and not asking twiceLI-BL-CMS-008 |
product |
Product | Sales Order line | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | One request per order, every late item on itLI-BL-CMS-006 |
expected_ship_date |
Expected Ship Date | Sales Order line | Saved search (id unconfirmed — see DN-Q1) | date | read | One request per order, every late item on itLI-BL-CMS-006 Recording the call, and not asking twiceLI-BL-CMS-008 |
custcol_delay_ship_date |
Delay Ship Date | Sales Order line | NetSuite | date | read-written | One request per order, every late item on itLI-BL-CMS-006 Recording the call, and not asking twiceLI-BL-CMS-008 |
original_lead_time |
Original Lead Time | Sales Order line | Saved search (id unconfirmed — see DN-Q1) | date | read | One request per order, every late item on itLI-BL-CMS-006 |
days_delayed |
Days Delayed | Sales Order line | Saved search (id unconfirmed — see DN-Q1) | integer | read | One request per order, every late item on itLI-BL-CMS-006 |
customer_internal_id |
Customer Internal ID | Customer | Saved search (id unconfirmed — see DN-Q1) | integer | read | Which delays are handled by a callLI-BL-CMS-005 Who the call lands withLI-BL-CMS-007 |
customer_type |
Customer Type | Customer | Saved search (id unconfirmed — see DN-Q1) | list | read | Who the call lands withLI-BL-CMS-007 |
kustomer_type_tree |
needs confirming | Kustomer customer | Kustomer | free-form text | read | Who the call lands withLI-BL-CMS-007 |
email_address |
Email Address | Customer | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_phone |
Customer Phone | Customer | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_first_name |
Customer First Name | Customer | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_last_name |
Customer Last Name | Customer | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | Which delays are handled by a callLI-BL-CMS-005 |
name1 |
Name1 | Customer | Saved search (id unconfirmed — see DN-Q1) | list | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_company_name |
Customer Company Name | Customer | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_fax_id |
Customer Fax ID | Customer | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_initial_contact_channel |
Customer Initial Contact Channel | Customer | Saved search (id unconfirmed — see DN-Q1) | list | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_shopify_store |
Customer Shopify Store | Customer | Saved search (id unconfirmed — see DN-Q1) | list | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_application_approval |
Customer Application Approval | Customer | Saved search (id unconfirmed — see DN-Q1) | list | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_accepts_marketing |
Customer Accepts Marketing | Customer | Saved search (id unconfirmed — see DN-Q1) | checkbox | read | Which delays are handled by a callLI-BL-CMS-005 |
customer_address |
Customer Address Line 1 / Line 2 / City / State / Zip | Customer | Saved search (id unconfirmed — see DN-Q1) | free-form text | read | Which delays are handled by a callLI-BL-CMS-005 |
Outputs
| Field id | Field name | Record | Source | Type | Read / written | Used by |
|---|---|---|---|---|---|---|
custcol_delay_ship_date |
Delay Ship Date | Sales Order line | NetSuite | date | read-written | One request per order, every late item on itLI-BL-CMS-006 Recording the call, and not asking twiceLI-BL-CMS-008 |
custcol_delay_comms |
Delay Comms | Sales Order line | NetSuite | list — CUSTOMLIST_DELAY_COMMS | written | Recording the call, and not asking twiceLI-BL-CMS-008 |
Hover any box and its explanation appears beside it. Click to pin it here.