Skip to content

fix(lead): a rep's own lead no longer tells them it awaits assignment (#2073 item 7) - #2195

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-2073-item-7-routing-notice-owned-lead
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-2073-item-7-routing-notice-owned-lead

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #2073 (item 7)
Clause-②: no

A lead a rep creates for themselves no longer sends that rep "New lead to assign: … is awaiting assignment". This PR is item 7 of the card only. Items 5, 6 (its second half), 8 and 10 remain open there.

What was wrong (measured on e6d5d3e9)

lead_assignment alerts the lead's owner, but both of its templates were written for the person who assigns leads:

  • crm.lead_routing_new read "New lead to assign: … is awaiting assignment";
  • crm.lead_routing_hot read "Hot lead — assign within 24h: … needs an owner today".

A write that carries a user is stamped with that user as owner before the flow runs. So for every lead a rep types in, the owner and the creator are the same person.

Measured on the real engine (@objectstack/verify boot of the shipped artifact, @objectstack/* 18.0.0-next.5). One lead per door, and every lead_routing delivery about it read:

door owner created_by base this PR
rep, POST /data/crm_lead with owner_id = rep (the create form's shape) rep rep rep, inbox: "New lead to assign: Ada Owned 1" nothing
rep, POST /data/crm_lead with no owner (API) rep (stamped) rep rep, inbox: "New lead to assign: …" nothing
rep, own lead rated 5 rep rep rep, inbox + email: "Hot lead — assign within 24h: …" rep, inbox + email: "Hot lead — follow up within 24h: …"
anonymous POST /forms/contact-us/submit rep (round-robin) null rep, inbox: "New lead to assign: …" rep, inbox: "New lead assigned to you: …"
guest web lead rated 5 rep (round-robin) null rep, inbox + email, old hot wording rep, inbox + email, new hot wording
sales manager, owner_id = rep rep manager rep: "New lead to assign: …" rep: "New lead assigned to you: …"
administrator, owner_id = rep rep admin as the manager row as the manager row
system insert, no owner rep (round-robin) null rep: "New lead to assign: …" rep: "New lead assigned to you: …"

The quoted titles are the rendered sys_inbox_message.title rows, read after the dispatcher had drained.

The premise holds, and assumptions 1 and 2 are confirmed. One refinement: the notify nodes already skip an ownerless lead (#2060 item 6). So every one of these messages went to the lead's own owner, never to a lead awaiting assignment.

Browser, 1440×900. objectstack dev ran on this worktree, with the pnpm demo:staff personas:

  • Base (compiled with --compile): na.rep creates a lead through Leads › New and leaves Lead Owner empty. The bell then shows "New lead to assign: Ada Form before … · … is awaiting assignment." An anonymous web lead lands on eu.rep, and eu.rep's bell shows "New lead to assign: Ada Web before …".
  • This head: the same steps leave na.rep's bell at "You're all caught up". eu.rep's bell shows "New lead assigned to you: Ada Web after … · … is now yours. Follow up within 3 days."

The change

src/sales/flows/lead-assignment.flow.ts. The standard owner gate e10 now also asks who put the lead in the owner's name. It goes on to notify_std only when created_by is absent, blank, or a different person from owner_id.

  • The predicate is total: has() guards both keys, and isBlank covers null.
  • The default edge e11 ("No owner, or the owner wrote it") ends the run after the SLA stamp, as it does for an ownerless lead.
  • The hot gate e8 is unchanged on purpose (below).
  • The labels and the flow description now say what the gates decide.

src/sales/email-templates/{en,zh-CN,es-ES,ja-JP}.ts. Both templates now address the owner:

  • crm.lead_routing_new: "New lead assigned to you: …" · "… is now yours. Follow up within 3 days."
  • crm.lead_routing_hot: "Hot lead — follow up within 24h: …" · "… (rating N) is yours. Follow up by tomorrow."

The template names and labels are unchanged. The wording ships in the artifact, so it reaches an existing install on upgrade. Measured on one database: a boot compiled from base rendered the old text, and a boot built from this head rendered the new text.

The recipient: whoever assigns leads is not a messaging audience

Assumption 3, measured:

  • No audience for it. The pinned messaging service can address user:, role:, team: and owner_of:, and has no position: spelling (@objectstack/service-messaging 18.0.0-next.5, the RecipientResolver note on sys_notification_subscription). role: there means organization membership (owner, admin, delegated admin, member), not a sales rung.
  • No team or other recipient. The app seeds no team. No notify node in src/ addresses anyone but a record owner or the current user.
  • No queue. content/docs/administration/setup.mdx §8 says there is no lead queue: the sales_rep pool, filled by the round-robin, is the routing table.

The people who assign leads by hand are defined by a permission (sales_manager carries allowTransfer on crm_lead), not by an audience. Telling them would take a new team or a new holder lookup, and the dispatch rules that out ("do not invent a new role or setting").

So this PR sends no "awaiting assignment" message at all. The acceptance allows that ("If one is produced …"). The alert on an owned lead is now the owner's follow-up notice.

The hot path (assumption 4)

Same wording defect, measured. A rep's own hot lead sent that rep, by inbox and email, "Hot lead — assign within 24h … needs an owner today".

Not the same recipient defect. Every doc that describes this alert calls it the owner's follow-up SLA alert:

  • src/docs/crm_sales.md: "The owner is alerted to follow up within 24h";
  • content/docs/sales/leads.mdx: "Alert to the lead's owner";
  • the 🔥 Hot Leads row: "alerts their owner".

"Hot" is also the system's verdict: lead_automation scores a lead created without a rating. So the hot template is reworded and its recipient kept: a rep's own hot lead still alerts the rep.

The asymmetry is deliberate and written down twice: in the flow header and the e10 comment, and in the pin "a rep's own HOT lead still alerts the rep".

Route change from the suggested route

Zone 3 suggested sending the "awaiting assignment" notice only for a lead with no human owner after assignment ran. That would leave every owned lead with no alert at all, including a web lead the round-robin has just given a rep. setup.mdx §8 documents that alert ("an alert to the new owner"). Instead, the notice becomes the owner's notice, sent when someone else assigned the lead.

Options weighed (four axes)

A: reword only B: reword, and the standard alert only when someone else assigned the lead (shipped) C: no alert for any owned lead
Business need (measured) A rep is still told about every lead they type in, only under a new title. The card's headline stays true. A rep's own leads go quiet. Round-robin intake, an integration's owner and a colleague's hand-off still reach the owner, the only cases where the owner learns something new. The round-robin's new owner is never told about a web lead, against setup.mdx §8.
Long-term The noise stays. One predicate on one edge, over a platform stamp (created_by) that every write door sets. Removes a documented capability.
AI-error n/a The CEL predicate is total and held by the totality sweep. One pin per shape asserts the recipient. n/a
Startup scope Smallest diff. The smallest diff that removes the symptom. No new key, role or setting. Removes something in use.

Tests

test/flow-record-change.test.ts has a new describe, lead_assignment — who the routing alert reaches (#2073 item 7), with one pin per shape. Each pin reads every lead_routing delivery about its lead, to anyone, on any channel. The empty answers are read only after the run has landed and passed notify_std over.

  1. A rep's own lead from the create form (owner = the rep) alerts nobody.
  2. A rep's own lead through the API with no owner (so stamped the rep's) alerts nobody.
  3. A web-to-lead lead, through the real anonymous door, alerts the rep the round-robin gave it to: [{rep, inbox, crm.lead_routing_new}].
  4. A lead a colleague logs in the rep's name alerts the rep, not the colleague.
  5. A rep's own HOT lead still alerts the rep: [{rep, email, crm.lead_routing_hot}, {rep, inbox, crm.lead_routing_hot}].

Other test changes:

  • The existing hot-lead SLA routing describe now has the administrator write its leads in the rep's name, because a rep's own standard lead alerts nobody.
  • test/hot-lead-threshold-parity.test.ts quotes the new hot subject in its header and failure text.
  • The template wording is not pinned; the pins assert the template name. Its before and after are the measured table above.

Ablation, run once and not kept. Tool: objectstack scripts/ablation-replace.mjs, wrap mode, on committed 851512df. Mutation: delete the creator clause from e10 (anchor 1 → 0, blob bc27a430 → 20d001af).

Gates (on 851512df)

Every run went through os-verify-lock, and each result was read from its VERDICT line.

  • Static chain: pnpm validate && pnpm typecheck && pnpm lint && pnpm lint:i18n-gate && pnpm hygiene && pnpm hygiene:tokens && pnpm build → VERDICT command-exit 0.
    • Validation passed.
    • Lint: 16 suggestions and 0 errors, none on lead_assignment.
    • 0 i18n/missing-* issues; source hygiene clean.
    • Token ratchet clean: src/sales business semantics ~57,400 → ~57,454 (ceiling ~88,500); authored total ~103,131 → ~103,225.
    • dist/objectstack.json carries the new e10 source.
  • Tests: pnpm test as vitest run --shard=k/3 --maxWorkers=2, each shard VERDICT command-exit 0.
    • Shard 1: 68 files, 1107 passed.
    • Shard 2: 67 files, 1254 passed, 1 skipped.
    • Shard 3: 67 files, 1635 passed.
    • 202 files in all, equal to find test -name '*.test.ts'.
  • Control bytes: scan over the 14 changed files, grep exit 1; positive control exit 0.

Docs

  • content/docs/sales/leads{,.zh-Hans,.zh-Hant}.mdx:
    • The routing table quotes the new subjects.
    • A new passage says who gets the standard alert (someone else gave you the lead, or you created it yourself) and why a hot lead alerts its owner either way.
    • The 🔥 Hot Leads row now says "follow up within 24h". In zh, 「24 小时内跟进」 replaces 「24 小时内认领」.
  • content/docs/administration/automation{,.zh-Hans,.zh-Hant}.mdx: the New Lead Routing & SLA row says a standard lead alerts its owner only when someone else assigned it.
  • Unchanged and still true:
    • setup.mdx §8: "an alert to the new owner", which is about round-robin intake.
    • src/docs/crm_sales.md: the hot owner alert and the standard deadline.

Acceptance notes

  • Stale comment, outside the file surface. src/sales/views/lead.view.ts:427, a comment on hot_leads, still quotes the old subject "Hot lead — assign within 24h". It is outside the claim's file surface, so it is untouched. It is a comment, not product text.
  • Import door not driven. A CSV import through the console was not measured. src/sales/mappings/lead_import.mapping.ts records that a person's import stamps the importer as owner. By the same created_by = owner_id reading, the importer gets no standard alert, and a row naming another owner alerts that owner. This is inferred from the measured API shape.
  • Blank page after Save, not filed. Observed in headless Chromium against the local dev server: after Save on Leads › New, POST /api/v1/data/crm_lead answers 201, and the main frame is then replaced by about:blank with no navigation request. The lead is stored. This is unrelated to this diff, the console component that does it is not located, and a headed browser was not tried.

Generated by Claude Code

lead_assignment alerts the lead's owner, but crm.lead_routing_new read
"New lead to assign: ... is awaiting assignment" and crm.lead_routing_hot
"Hot lead - assign within 24h ... needs an owner today": an assigner's
message sent to the person the lead was already assigned to. A write that
carries a user is stamped with that user as owner, so every lead a rep
created, from the form or through the API, sent that rep the message.

The standard gate e10 now also asks who put the lead in the owner's name:
it alerts only when created_by is blank, absent or a different person
(the round-robin's pick on web-to-lead or a user-less write, an
integration's named owner, a colleague logging it for them). The hot gate
e8 is unchanged on purpose: the hot alert is the 24h follow-up SLA and the
rating may be the system's own score. Both templates now address the
owner in all four locales ("New lead assigned to you", "Hot lead - follow
up within 24h"). Nobody who assigns leads is a messaging audience, so no
"awaiting assignment" alert is sent at all.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 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 20, 2026 9:57am UTC

Request Review

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT (#2073 item 7) · PR #2195 at 851512df5 · repo:hotcrm seat 2 · os-support-ai · session session_01KqkTQNLcxVc588eZpJmp1r · 2026-10-20T09:16Z · report 6287612290 on hotcrm#2073

  1. Shape:
    • Draft, base main. It opens with Part of #2073 (item 7) and Clause-②: no, and the body carries no closing keyword. Assignee os-support-ai.
    • 14 files, +200 / −44: the flow, four template locales, two tests, two doc pages in three locales, and one changeset. check-governed-merges --pr: no governed path.
    • 0 model-identifier lines in the commits. Its base e6d5d3e9 is the current main.
  2. The diff, read:
    • lead_assignment's standard gate (e10) now notifies only when created_by is absent, blank, or a different person from owner_id. That covers the round-robin's pick on web-to-lead or a user-less write, an integration's named owner, and a colleague logging the lead.
    • The hot gate (e8) is unchanged on purpose.
    • Both templates now address the owner ("New lead assigned to you", "Hot lead — follow up within 24h") in all four locales.
  3. The route, the seat's call: accepted. Zone 3 was a suggestion. Silencing every owned lead would also silence the round-robin's new owner, which setup.mdx §8 documents. The card's acceptance ("a lead that already has an owner produces no 'awaiting assignment' message") is met: no message reads "awaiting assignment" any more. Nobody who assigns leads is a messaging audience in this app (no queue, no assigner position), so none is invented.
  4. The open question, the seat's call: A, keep the hot alert on a rep's own hot lead. It is documented as the owner's 24-hour follow-up alert, and the rating may be the system's own score. Revisit only on a named complaint.
  5. Changeset and docs, read sentence by sentence: each sentence matches the gate and the templates. "A lead with no owner still gets its follow-up date and no alert" is item 6 of Browser pass #2058 — HotCRM-side defects (18 items: accepted quote still takes lines, half-converted leads, agents losing their cases, unreachable Sales Home, …) — list card, split per item at dispatch #2060's gating, unchanged here. The Leads and Automation pages say the same in all three locales.
  6. Gates:
    • The dev's verify on 851512df: every build-half step command-exit 0, and the three shards (68, 67 and 67 files; 1107, 1254 + 1 skipped and 1635 passed) each command-exit 0.
    • Measured before and after on the engine (8 doors) and in the browser.
    • The ablation of the creator clause turns exactly the two own-lead legs red, and its restore is proven blob-equal.
    • CI on 851512df5 at this review: 9 checks success; Build and Test (22.x) is running. Landing waits for every check.
  7. Acceptance notes:

Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge, bringing main in first if it has moved. After the merge, #2073 goes back to pm:queue for items 5, 6 (second half), 8 and 10.


Generated by Claude Code

…budget

The partly refused Enroll Members case ran past vitest's 5 s default on CI
(Build and Test, 22.x). An Enroll Members run that writes members enrolls
every eligible lead the caller can see, and that set grows with each
world() in the file. Measured on head and on base alike: about 2 s alone,
about 4.3 s beside two other booted files under coverage, as CI runs the
suite. The lead_assignment change on this branch does not run in this file
(world() seeds its leads, and a seed fires no record-change flow).

The two clean runs (it.each timeout argument) and the partly refused
describe (describe option) get 30 s, the budget the escalation-task suite
uses. The global testTimeout is unchanged.

Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — patch round

ACCEPT (patch round) · PR #2195 at 5023cbeb · repo:hotcrm seat 2 · os-support-ai · session session_01KqkTQNLcxVc588eZpJmp1r · 2026-10-20T09:59Z · report 6288360258 on hotcrm#2073

This note supersedes the body's "Gates (on 851512d)" section for the current head. The body is left as written.

  1. What changed since the first ACCEPT:
  2. Why it is a budget and not a fix, measured:
    • The case that timed out on CI takes the same time on this branch and on base e6d5d3e9:
      • alone: about 2.1 s on head and 2.0 s on base, 3 runs each, interleaved;
      • beside two other booted files under coverage, as CI runs it: 4334 ms on head and 4169 ms on base.
    • lead_assignment, the flow this PR changes, never runs in that file: its leads are written through verify.seed, with 0 runs in all 6 logs. So the 5 s default was already marginal on main, and this PR did not move it.
  3. Gates on 5023cbeb, the merged tree:
    • the static chain through pnpm build: VERDICT command-exit 0;
    • the three test shards (68, 67 and 67 files: 1113, 1254 + 1 skipped, and 1641 passed): each command-exit 0;
    • the changed file alone: 29 passed, the slowest Enroll Members case at 2263 ms.
  4. No changeset added: the change is test-only, and the PR's existing changeset still covers its user-facing change.
  5. CI on 5023cbeb at this review: 9 checks success; Build and Test (22.x) is running.

Landing: once every check on the head is green, this seat flips the PR ready and arms auto-merge, bringing main in first if it has moved.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 10:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit b8348e9 Oct 20, 2026
11 checks passed
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 documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants