Skip to content

fix(flows): a sales rep converts a lead into a new account; a system sub-flow copies the revenue - #2102

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-2016-rep-conversion-system-subflow
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-2016-rep-conversion-system-subflow

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #2016

Clause-②: no

What this fixes

A sales rep converting a lead whose company has no account yet was refused at create_account with FLOW_FAILED / 400, "Field write denied: not permitted to edit [annual_revenue] on 'crm_account'". Nothing was written. The node copied the lead's annual_revenue, and sales_rep.profile.ts makes crm_account.annual_revenue read-only. Re-measured on e094ec71 (18.0.0-next.0) before any edit. The refusal is the same with and without a revenue on the lead.

The change (ruling 6076798874, letter D, now "build B")

  • lead_conversion stays runAs: 'user'. create_account is still the converting user's own write. It no longer carries annual_revenue.
  • New lead_conversion_account_revenue (src/sales/flows/lead-conversion-account-revenue.flow.ts), type: 'autolaunched', runAs: 'system'. It runs one update_record that writes one column, annual_revenue, onto the account id it is handed. It reads nothing.
  • A new subflow node, copy_account_revenue, calls it right after create_account with { accountId: createdAccount.id, annualRevenue: guarded leadRecord.annual_revenue }. Edge e7 is retired and stays vacant. e41 and e42 route create_account, then copy_account_revenue, then use_new_account. No other node or edge moved.
  • The flow is registered in the sales barrel and in appFlows (objectstack.composition.ts). The automation page lists it in en, zh-Hans and zh-Hant, and the flow count goes from 32 to 33 in README.md and docs/STATUS.md.
  • sales-rep.profile.ts is unchanged. The annual_revenue lock stays, and a test pins it.

Why the sub-flow copies the revenue and does not create the account

The ruling's wording is "the account is created through a dedicated runAs: 'system' sub-flow". I built and measured that shape first (scratch prototype, not committed). The system context skips the object gate as well as the field lock. So a persona that may read leads but may not create accounts gets an account written past allowCreate: false. The run then fails at create_contact and leaves that account behind.

persona, conversion into a NEW company e094ec71 (today) whole create in the system flow this PR (revenue copy only)
sales rep FLOW_FAILED at create_account (FLS), 0 accounts converts; revenue 4,200,000; owner = created_by = rep converts; revenue 4,200,000; owner = created_by = updated_by = rep
sales manager converts converts converts
marketing user (crm_account.allowCreate: false) FLOW_FAILED at create_account, 0 accounts FLOW_FAILED at create_contact, 1 orphan account owned by the marketing user FLOW_FAILED at create_account, 0 accounts
service agent (allowCreate: false) FLOW_FAILED at create_account, 0 accounts FLOW_FAILED at create_contact, 1 orphan account FLOW_FAILED at create_account, 0 accounts
account approval request snapshot (payload_json.annual_revenue) manager: 4,200,000 not measured (one write, so it would carry it) null, for rep and manager alike

AGENTS.md house rule 9 says to elevate as little as possible, and that is the ruling's own basis. Copying only the revenue follows it, in the same shape as case_escalation_stamp. The cost is the last row. account_approval opens the request on the insert, before the copy lands, so the request's submission snapshot shows no annual revenue. The account itself carries it. In the console's Approvals Inbox, that request's row shows no amount. An account created with its revenue in one write shows Account· 4,200,000 (measured below). This trade-off goes to the seat as an open question in the report. If the maintainer prefers the whole create, the change is about 30 lines, and the marketing-user test below is the one that goes red.

Doors (18.0.0-next.0)

The trigger door POST /automation/lead_conversion_account_revenue/trigger (verify.flows.run) refuses a direct start with PERMISSION_DENIED / 403. Measured callers: a sales rep, a member with no permission set, a marketing user and the admin. No account changed. In the installed @objectstack/runtime dist, the action door (dispatchFlowAction) and the declared flow endpoint (executeFlow) call the same refusesElevatedSelfTriggeredStart. No action or endpoint in this app names the new flow, so neither door reaches it here.

The new account's organization_id is the converting user's (contextFor(token).tenantId), and the test pins it. The elevated node is a single update by id, not a scan or roll-up (house rule 10). The account was created by the user's own write in their organization.

Tests

test/flow-conversion.test.ts gains a #2016 block:

  • A rep with revenue, a rep without revenue, and a manager each convert into a new account: 1 account, contact and opportunity; annual_revenue = the lead's; owner_id = created_by = the converter; organization_id = the converter's organization.
  • The lock stands: the rep's own update of annual_revenue on that account is refused PERMISSION_DENIED / 403, and the value is unchanged.
  • A rep and the admin each start lead_conversion_account_revenue directly and are refused PERMISSION_DENIED / 403. The account keeps its revenue.
  • A marketing user's conversion into a new company: FLOW_FAILED / 400, 0 accounts, lead not converted.

Reverse verification and ablations. Each mutation was restored by git checkout HEAD -- or ablation-replace, with git diff HEAD empty afterwards.

  • Base flow and registration from e094ec71, new tests unchanged. Predicted 5 red, 2 green. Measured 5 red: both rep conversions, the lock test and both door tests (RESOURCE_NOT_FOUND 404, since the flow is absent). The manager and marketing cases stayed green.
  • Callee runAs: 'system' changed to 'user' (ablation-replace, anchor 1 to 0, blob changed, restored blob equals HEAD). Predicted 5 red. Measured 5 red: rep conversions fail at copy_account_revenue, the rep's direct start is admitted (FLOW_FAILED 400), and the admin's direct start is admitted and succeeds (refusal null).
  • Whole create in the system flow (the prototype above, applied over HEAD). Predicted only the marketing case red. Measured 1 red, 6 green: received accounts: 1.

Gates

One pnpm verify run on HEAD 4e876995, under the shared verify lock: VERDICT command-exit 0.

  • pnpm validate: ✓ Validation passed, Logic: 33 Flows. The 10 warnings were there before this change (the hierarchy-security capability, approval slates). None names a conversion flow.
  • pnpm typecheck: tsc --noEmit, exit 0. --listFiles includes test/flow-conversion.test.ts.
  • pnpm lint: 18 suggestion(s), exit 0. None names a conversion flow.
  • pnpm lint:i18n-gate: ✓ i18n lint gate: 0 i18n/missing-* issues
  • pnpm hygiene: ✓ source hygiene clean
  • pnpm hygiene:tokens: ✓ src/sales authored total ~102,954 tokens (ceiling ~160,500; headroom ~57,546), ✓ source token ratchet clean
  • pnpm build: ✓ Build complete, Logic: 33 Flows
  • pnpm test: Test Files 179 passed (179), Tests 3666 passed | 1 skipped (3667)

The Playwright e2e suite is CI's.

Console check (my worktree on port 4106, after pnpm demo:staff, DOM probes)

  • As na.rep@objectos.ai (sales rep): I created a qualified lead for a new company with Annual Revenue 4,200,000. On its record page: Convert Lead, tick Create Opportunity?, Amount 40000, Submit.
  • Network: POST /api/v1/actions/crm_lead/convert_lead returned 200, then …/lead_conversion/runs/RUN/resume returned 200.
  • The dialog closed with the toast "Lead converted successfully!". The text "Field write denied" is nowhere on the page.
  • Read back: the lead is converted. Its account has annual_revenue 4200000, owner_id = created_by = updated_by = na.rep, and approval_status pending.
  • As sales.manager@objectos.ai, in the Approvals Inbox, that account's row reads Account with no amount. A control account the manager created with the revenue in one write reads Account· 4,200,000. That row is the cost described above, measured.

Acceptance notes

  • The comment in test/account-approval-gate.test.ts (around L186) still explains its manager converter by the rep refusal "on 17.7.0". That is dated and true, so it stays. Nothing there asserts the refusal.
  • Files touched outside the claimed surface, each needed by a guard: content/docs/administration/automation{,.zh-Hans,.zh-Hant}.mdx and test/automation-docs-coverage.test.ts (every shipped flow has a row), README.md and docs/STATUS.md (the registered flow count), test/flow-variable-conditions.test.ts (its end-to-end harness registers flows by name, so the two lead_conversion cases now register the callee too).

Generated by Claude Code

…ue is copied by a system sub-flow

lead_conversion's create_account wrote annual_revenue, which the sales_rep
permission set makes read-only, so every rep's conversion into a new account
failed with "Field write denied ... [annual_revenue]" and nothing was written.

create_account (still the converting user's own write) no longer carries the
column. A new runAs: 'system' flow, lead_conversion_account_revenue, writes
the lead's revenue onto the account that write created, called through a
subflow node (AGENTS.md house rule 9). Since 18.0.0-next.0 no door that
starts a flow by name admits a non-system caller to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
…r the rep conversion fix

The automation page's built-in flow table gains the new sub-flow in all
three locales (33 flows), and the coverage ledger carries its two Chinese
row labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
…ts callee

The account's approval request is opened on the insert, so its submission
snapshot carries no annual revenue; the account itself does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
…s the revenue callee

README and docs/STATUS.md state the registered flow count, which the new
sub-flow moves from 32 to 33. The end-to-end lead_conversion cases in
flow-variable-conditions register flows by name, so they now register the
callee the conversion's new-account branch calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
@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 12:42pm UTC

Request Review

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

Seat repo:hotcrm, session_01P8mZuzuyZAHSrgQKmv6ASv, R86: ACCEPT under the maintainer's ruling A on #2016 (6168552280).

  • Form: draft, base main; the body opens Fixes #2016, with Clause-②: no; no other closing keyword. Not a governed path; 13 files, +279 −24. The head is 0d3e9a37, the relay's merge of main f2b679af into the reviewed 4e876995; the trial merge was clean.
  • Premise, measured by the dev, checked against the diff (the review 6165657983):
    • create_account stays the converting user's write and drops annual_revenue.
    • A new runAs: 'system' sub-flow lead_conversion_account_revenue writes that one column onto the id copy_account_revenue hands it.
    • Direct starts of the sub-flow are refused (403) for a rep, a no-grant member, a marketing user and the admin.
    • A rep's conversion into a new account now succeeds with the revenue copied; the sales_rep lock on annual_revenue is unchanged and pinned.
  • Gates, read off the head's check-runs on 0d3e9a37: CodeQL ✓ · Analyze Code (javascript) ✓ · Build and Test (22.x) ✓ · Typecheck and Build ✓ · Quality Checks ✓ · Check Changeset ✓ · Playwright ✓ · link-check ✓ · Label Pull Request ✓ · Vercel Preview Comments ✓. The dev's local pnpm verify was green on 4e876995.
  • Deviations accepted:
    • Outside the claim, forced by guards: the automation docs rows (3 locales) with their coverage pin, and the flow count 32 → 33 in README.md / docs/STATUS.md.
    • The route narrowed from the ruling's wording; the maintainer ruled A.
    • Four commits carry a model-named trailer; history is not rewritten (os-dev rule).
  • Out of scope:
  • Landing: every check green on the reviewed head ⇒ ready + auto-merge through the merge queue.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 12:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 850f75c Oct 14, 2026
11 checks passed
sunhaolin pushed a commit that referenced this pull request Oct 14, 2026
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 configuration Build and app configuration files documentation Improvements or additions to documentation

Projects

None yet

2 participants