Skip to content

fix(lead): an ownerless lead's routing run ends after its SLA stamp instead of failing - #2101

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-2060-item-6-ownerless-lead-assignment
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-2060-item-6-ownerless-lead-assignment

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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_assignment fail. The card stays open for its other items.

What was wrong (measured on base 4f389428, @objectstack/* 18.0.0-next.0)

lead_assignment sends its alert to record.owner_id on 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.

  • Public HTTP door, own server (objectstack dev -p 4107 --fresh, artifact compiled from the base flow): an anonymous POST /api/v1/forms/contact-us/submit (the route /_console/f/contact-us posts to) answered 201; the lead was stored with owner_id: null, lead_source: web, rating: 1, next_followup_date: 2026-10-17; its sys_automation_run row read status: 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 a decision where no recipient is expected."
  • In process (the verify-booted app): the same failure for a guest web-to-lead insert, and for a stored ownerless lead on the hot branch. A second failure mode shows up when the key is ABSENT (driver-memory / driver-mongodb store only the columns written): "recipients[0]: value expression failed to evaluate as CEL: No such key: owner_id".

No recipient for ownerless intake is declared anywhere: content/docs/administration/setup.mdx §8 says lead routing has "no territory routing, no queue", and src/docs/crm_sales.md leaves 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 gateway decision after its SLA stamp (owner_hot, owner_std, label "Has an Owner?"):

  • e8 / e10 → the notify node, conditional on has(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 decision without config.conditions reports no branch label, so traversal takes the first conditioned out-edge that holds and the default edge only when none does (read in service-automation traverseNext and the decision executor; this is also the shape the platform lint's own hint names). isBlank is true for exactly what notify refuses as no recipient (null, a blank string, an empty list), and has() covers the absent key. The hot/standard split stays the single rating cut on e2 / 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)

  • Anonymous POST /api/v1/forms/contact-us/submit → 201; lead owner_id: null, next_followup_date: 2026-10-17; run status: completed, error: null, steps start:success check_hot:success sla_hot:skipped sla_std:success owner_std:success notify_std:skipped; lead_routing deliveries: 0.
  • Control, an owned lead (admin POST /api/v1/data/crm_lead, rating: 5): run completed, owner_hot:success notify_hot:success, 2 lead_routing deliveries (email, inbox), both to the lead's owner.

Tests

test/flow-record-change.test.ts:

  • New lead_assignment — a lead with no owner (#2060): four cases, rating 5 / 1 × owner_id null / 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 reads completed, the SLA stamp landed, the branch's notify node is skipped, and no lead_routing delivery 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).
  • The existing web-to-lead case (lead_assignment stamps the SLA on a web-to-lead submission) now also asserts the run's status: '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 (recordTerminal is 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.
  • An owned lead still alerting its owner on both branches is the existing lead_assignment — hot-lead SLA routing cases and lead_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 blob bf401d7a = base blob, owner gates on disk: 0), and vitest run --reporter=verbose test/flow-record-change.test.ts -t lead_assignment ran. 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 with git checkout HEAD -- …; restored blob 72b40f4d = HEAD blob, git diff HEAD empty. The script carried a trap restore.

Gates (head 350cecd8, after merging origin/main 6782f782)

gate exit verdict line
pnpm validate 0 ✓ Validation passed
pnpm typecheck 0 tsc --noEmit (no output)
pnpm lint 0 18 suggestion(s), none on lead_assignment
pnpm lint:i18n-gate 0 ✓ i18n lint gate: 0 i18n/missing-* issues
pnpm hygiene 0 ✓ source hygiene clean
pnpm hygiene:tokens 0 ✓ source token ratchet clean (src/sales business semantics ~58,052 / ~88,500)
pnpm build 0 ✓ Build complete
pnpm test 0 Test 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 0 on each). Playwright e2e is left to CI.

Acceptance notes


Generated by Claude Code

…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>
@vercel

vercel Bot commented Oct 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 14, 2026 9:23am UTC

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline backend Server-side behaviour — hooks, flows, actions labels Oct 14, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 09:36
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit c0f3485 Oct 14, 2026
10 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants