Repository navigation
fix(lead): an ownerless lead's routing run ends after its SLA stamp instead of failing - #2101
Merged
objectstack-fleet[bot] merged 3 commits intoOct 14, 2026
Conversation
…nstead of failing lead_assignment alerted record.owner_id on both branches with nothing in front of the notify node, so a lead with no owner (web-to-lead whenever lead_auto_assign stands down, a seed or integration write) failed the run at notify_hot / notify_std after the SLA stamp had landed: "notify: at least one recipient is required ... record.owner_id". Each branch now passes through a gateway decision after its SLA stamp: an owned lead goes on to its alert, and the isDefault "No owner" edge ends the run. The hot/standard split stays the single rating cut on e2 / e3. Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv Co-authored-by: Claude <noreply@anthropic.com>
…ment Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
sunhaolin
pushed a commit
that referenced
this pull request
Oct 14, 2026
#2101's new ownerless-lead describe said web-to-lead lands ownerless when the anonymous form grant cannot read the rep pool; since the intake hooks read it elevated, only an empty pool or a failed read leaves it ownerless. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
This was referenced Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #2060
Clause-②: no
Item 6 only (finding A-08 / C-20, reproduced on 18.0.0-next.0 as #2093 finding 01): an ownerless lead makes
lead_assignmentfail. The card stays open for its other items.What was wrong (measured on base
4f389428,@objectstack/*18.0.0-next.0)lead_assignmentsends its alert torecord.owner_idon both branches (notify_hot,notify_std), with nothing in front of the notify node. A lead with no owner therefore failed the run at the notify node, after the SLA stamp had already landed.objectstack dev -p 4107 --fresh, artifact compiled from the base flow): an anonymousPOST /api/v1/forms/contact-us/submit(the route/_console/f/contact-usposts to) answered201; the lead was stored withowner_id: null,lead_source: web,rating: 1,next_followup_date: 2026-10-17; itssys_automation_runrow readstatus: failed,node_id: notify_std, error: "Node 'notify_std' failed: notify: at least one recipient is required, but every recipient value expression evaluated to nothing:record.owner_id. … route around this node with adecisionwhere no recipient is expected."No recipient for ownerless intake is declared anywhere:
content/docs/administration/setup.mdx§8 says lead routing has "no territory routing, no queue", andsrc/docs/crm_sales.mdleaves an ownerless lead "for a manager to claim". So the fix does not invent one; per the card, "the flow should not fail when there is none".The change
src/sales/flows/lead-assignment.flow.ts: each branch now passes through a gatewaydecisionafter its SLA stamp (owner_hot,owner_std, label "Has an Owner?"):e8/e10→ the notify node, conditional onhas(record.owner_id) && !isBlank(record.owner_id)(label "Owned");e9/e11→end,isDefault: true(label "No owner").One predicate per gate plus a default fallback, not a hand-written complement, so the pair cannot drift. On 18.0.0-next.0 a
decisionwithoutconfig.conditionsreports no branch label, so traversal takes the first conditioned out-edge that holds and the default edge only when none does (read inservice-automationtraverseNextand the decision executor; this is also the shape the platform lint's own hint names).isBlankis true for exactly whatnotifyrefuses as no recipient (null, a blank string, an empty list), andhas()covers the absent key. The hot/standard split stays the single rating cut one2/e3; the SLA stamps are untouched. The stale header line "owner is set on insert (defaultValue os.user.id)" is replaced.After the change (head
86fa715e, same door, same server setup)POST /api/v1/forms/contact-us/submit→201; leadowner_id: null,next_followup_date: 2026-10-17; runstatus: completed,error: null, stepsstart:success check_hot:success sla_hot:skipped sla_std:success owner_std:success notify_std:skipped;lead_routingdeliveries: 0.POST /api/v1/data/crm_lead,rating: 5): runcompleted,owner_hot:success notify_hot:success, 2lead_routingdeliveries (email,inbox), both to the lead's owner.Tests
test/flow-record-change.test.ts:lead_assignment — a lead with no owner (#2060): four cases, rating 5 / 1 ×owner_idnull / absent, a stored ownerless lead handed to the flow the way the trigger hands it a write. Each asserts the run succeeds and its history row readscompleted, the SLA stamp landed, the branch's notify node isskipped, and nolead_routingdelivery names the lead. Written through the seed door so the case does not depend on how intake left the lead ownerless (Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014 F9 will change that for web-to-lead when the rep pool is not empty).lead_assignment stamps the SLA on a web-to-lead submission) now also asserts the run'sstatus: 'completed', the card's own reproduction at the in-process web door.landedRuns: the engine writes a finished run's history row after the run returns (recordTerminalis not awaited), and the first run of the new web-to-lead assertion read no row at all. The helper waits (vi.waitFor, 10 s) for the first row before asserting on it.lead_assignment — hot-lead SLA routingcases andlead_assignment alerts the owner of an integration-written lead, all green.Counter-proof (one-off, not kept): with the fix committed, the flow file was checked out at base
4f389428(on-disk blobbf401d7a= base blob, owner gates on disk: 0), andvitest run --reporter=verbose test/flow-record-change.test.ts -t lead_assignmentran. Direction set beforehand: red. Result: exit 1, 5 failed / 4 passed. The four new cases failed (null shape: "at least one recipient is required …record.owner_id"; absent shape: "No such key: owner_id"), the web-to-lead case failed with the card's message, and the owned cases stayed green. Restored withgit checkout HEAD -- …; restored blob72b40f4d= HEAD blob,git diff HEADempty. The script carried atraprestore.Gates (head
350cecd8, after mergingorigin/main6782f782)pnpm validate✓ Validation passedpnpm typechecktsc --noEmit(no output)pnpm lint18 suggestion(s), none onlead_assignmentpnpm lint:i18n-gate✓ i18n lint gate: 0 i18n/missing-* issuespnpm hygiene✓ source hygiene cleanpnpm hygiene:tokens✓ source token ratchet clean(src/salesbusiness semantics ~58,052 / ~88,500)pnpm build✓ Build completepnpm testTest Files 179 passed (179),Tests 3672 passed, 1 skipped (3673)The heavy gates ran one at a time through the shared verify lock (
VERDICT command-exit 0on each). Playwright e2e is left to CI.Acceptance notes
src/sales/objects/lead.hook.ts~L76 (not edited; Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014 F9 is working on that file) says an ownerless lead is left for "a manager / the lead_assignment flow" to route. After this change the flow still routes no ownerless lead to anyone. It stamps the SLA and sends no alert, so the "lead_assignment flow routes it" half of that sentence stays false. Left for Hooks that write or read across objects as the triggering person fail silently for reps, agents and web forms — 11 hook sites, one fix pattern (runAs), measured on the real engine by #2013 #2014 F9 to reword.src/revenue/flows/billing-handoff-contract-activated.flow.ts~L168–174 says anisDefault: truesibling on adecisionwith noconfig.conditions"would swallow the whole branch". That was true before the engine's A decision node has three declared ways to route a branch and two of them do nothing — app-crm's convert-lead guard runs both branches objectstack#4414. On 18.0.0-next.0 the decision executor reports no branch label in that case, and this PR depends on that and measures it. The comment is out of date. Not filed: no carrier.task_urgent_alertandopportunity_won_alertuse the same unguardedrecipients: [cel`record.owner_id`]. That is a read-only observation; no failing run of either was measured. Not filed.content/docs/sales/leads.mdxalready headlines the alert column "Alert to the lead's owner", and no page says an ownerless lead is alerted.Generated by Claude Code