Repository navigation
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 intoOct 14, 2026
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
This was referenced Oct 14, 2026
Contributor
Author
席位核对Seat
Generated by Claude Code |
sunhaolin
pushed a commit
that referenced
this pull request
Oct 14, 2026
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv Co-authored-by: Claude <noreply@anthropic.com>
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.
Fixes #2016
Clause-②: no
What this fixes
A sales rep converting a lead whose company has no account yet was refused at
create_accountwithFLOW_FAILED/ 400, "Field write denied: not permitted to edit [annual_revenue] on 'crm_account'". Nothing was written. The node copied the lead'sannual_revenue, andsales_rep.profile.tsmakescrm_account.annual_revenueread-only. Re-measured one094ec71(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_conversionstaysrunAs: 'user'.create_accountis still the converting user's own write. It no longer carriesannual_revenue.lead_conversion_account_revenue(src/sales/flows/lead-conversion-account-revenue.flow.ts),type: 'autolaunched',runAs: 'system'. It runs oneupdate_recordthat writes one column,annual_revenue, onto the account id it is handed. It reads nothing.subflownode,copy_account_revenue, calls it right aftercreate_accountwith{ accountId: createdAccount.id, annualRevenue: guarded leadRecord.annual_revenue }. Edgee7is retired and stays vacant.e41ande42routecreate_account, thencopy_account_revenue, thenuse_new_account. No other node or edge moved.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.tsis unchanged. Theannual_revenuelock 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 pastallowCreate: false. The run then fails atcreate_contactand leaves that account behind.e094ec71(today)create_account(FLS), 0 accountscrm_account.allowCreate: false)create_account, 0 accountscreate_contact, 1 orphan account owned by the marketing usercreate_account, 0 accountsallowCreate: false)create_account, 0 accountscreate_contact, 1 orphan accountcreate_account, 0 accountspayload_json.annual_revenue)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_approvalopens 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 showsAccount· 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 withPERMISSION_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/runtimedist, the action door (dispatchFlowAction) and the declared flow endpoint (executeFlow) call the samerefusesElevatedSelfTriggeredStart. No action or endpoint in this app names the new flow, so neither door reaches it here.The new account's
organization_idis 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.tsgains a #2016 block:annual_revenue= the lead's;owner_id=created_by= the converter;organization_id= the converter's organization.annual_revenueon that account is refusedPERMISSION_DENIED/ 403, and the value is unchanged.lead_conversion_account_revenuedirectly and are refusedPERMISSION_DENIED/ 403. The account keeps its revenue.FLOW_FAILED/ 400, 0 accounts, lead not converted.Reverse verification and ablations. Each mutation was restored by
git checkout HEAD --orablation-replace, withgit diff HEADempty afterwards.e094ec71, new tests unchanged. Predicted 5 red, 2 green. Measured 5 red: both rep conversions, the lock test and both door tests (RESOURCE_NOT_FOUND404, since the flow is absent). The manager and marketing cases stayed green.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 atcopy_account_revenue, the rep's direct start is admitted (FLOW_FAILED400), and the admin's direct start is admitted and succeeds (refusalnull).accounts: 1.Gates
One
pnpm verifyrun on HEAD4e876995, 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.--listFilesincludestest/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-* issuespnpm hygiene:✓ source hygiene cleanpnpm hygiene:tokens:✓ src/sales authored total ~102,954 tokens (ceiling ~160,500; headroom ~57,546),✓ source token ratchet cleanpnpm build:✓ Build complete,Logic: 33 Flowspnpm 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)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.POST /api/v1/actions/crm_lead/convert_leadreturned 200, then…/lead_conversion/runs/RUN/resumereturned 200.converted. Its account hasannual_revenue4200000,owner_id=created_by=updated_by= na.rep, andapproval_statuspending.sales.manager@objectos.ai, in the Approvals Inbox, that account's row readsAccountwith no amount. A control account the manager created with the revenue in one write readsAccount· 4,200,000. That row is the cost described above, measured.Acceptance notes
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.content/docs/administration/automation{,.zh-Hans,.zh-Hant}.mdxandtest/automation-docs-coverage.test.ts(every shipped flow has a row),README.mdanddocs/STATUS.md(the registered flow count),test/flow-variable-conditions.test.ts(its end-to-end harness registers flows by name, so the twolead_conversioncases now register the callee too).Generated by Claude Code