Release Notes — Week of 2026-06-08
Spans 2026-06-07 → 2026-06-08. 17 commits centred on four themes: lead qualification scoring (whole new score column with per-option editor), stage ownership (team round-robin + role-with-team scope + form submissions as a stage requirement), target attribution (give credit to the right person across SDR→AE handoffs), and an RBAC fix that decouples the admin and settings modules and adds a view-only mode.
Two tenant migrations ship with this release. Run them on prod with:
npx ts-node apps/api/src/scripts/run-tenant-migrations.ts
| Migration | Adds |
|---|---|
| 081 | leads.qualification_score (NUMERIC) + qualification_score_breakdown (JSONB); rebalances BANT and CHAMP framework field weights to 25/25/25/25; extends every option in field_options with an absolute score in [0, score_weight]. Idempotent. |
| 083 | Drops the old pipeline_stages.stage_owner_type CHECK and re-adds it with the new vocabulary (`inherit |
Lead Qualification Scoring
The qualification framework (BANT / CHAMP / your custom one) now produces a real numeric score on every lead — alongside the existing rule-based Lead Score.
What's new
- Per-option score editor in the Qualification tab of Lead Settings. Each field expands inline to edit its max weight + a score per option. Live validation enforces
option score ∈ [0, weight]andfield weights sum to 100. The active framework header shows the live total vs the required 100. qualificationScore+qualificationScoreBreakdowncomputed automatically on every lead create/update and on framework changes. Single-select fields sum the matched option; multi-select sums all matched options up to the field's weight.- Recompute All button on the active framework — bulk-rescores qualification across every lead in the tenant. Use after editing option scores.
- Auto-rescore on stage change —
changeStagenow recomputes the qualification score so the value on the detail page never goes stale after a stage move. - Shown everywhere:
- Lead detail page — a second score card next to Lead Score, with the framework name, a progress bar, and a per-field contribution breakdown.
- Lead list — a Qualification column (number + progress bar, same treatment as Score). Sortable, visible by default.
- Reports —
qualification_scoreis now a data-source field on leads. Filter, group, and aggregate on it.
- New workflow trigger —
lead_qualification_score_changedfires whenever the value transitions; independent fromlead_score_changedso each engine has its own automation hooks. - System fields registered —
system-scoreandsystem-qualificationScoreare now non-editable computed fields in the registry, so they appear in workflow condition pickers, page designer, and other registry consumers. qualification_scoreadded to update_field allowlist (for workflowupdate_fieldactions) and to the scheduled-scan filter allowlist (so dormancy-style scheduled workflows can target it).
See Lead Settings → Qualification, Lead Scoring & SLA, and Workflow Builder.
Stage Ownership
The biggest set of changes since the original Stage Ownership ship. The editor now exposes the full picker; the resolver supports two new modes.
New owner modes
| Mode | Picks via | Cursor |
|---|---|---|
inherit | Falls through to record owner. | n/a |
user | Single specific user (with an optional team filter that narrows the user dropdown — UX only, doesn't change resolution). | n/a |
team_lead | The team's designated lead. | n/a |
team_round_robin (new) | Rotates across the team's active members. | Per-stage cursor in stage_owner_team_assignments — so two stages sharing the same team rotate independently. |
role (with optional team scope) (enriched) | Round-robins across users with the role, optionally narrowed to a single team. Configure as "AEs on the EMEA team" instead of "all AEs globally". | Cursor keyed by role_id (in stage_owner_role_assignments). |
auto_assign is gone — migration 083 backfills any remaining rows automatically: with team_id → team_round_robin; without → inherit.
Form submission as a stage requirement
StageFieldsModal now has a Forms section listing every active form for the trigger module (GET /forms/module/:moduleName). Pick a form and the lead/opportunity can't advance to the stage until at least one matching submission exists in form_submissions for the entity.
Field key shape is form.<formId>. The validator (validateStageRequirements, now async on both leads and opportunities) treats this as a once-met-always-met requirement — backward stage moves keep the requirement satisfied, since the submission row is permanent.
Use cases:
- "Lead can't reach Qualified until the Demo Request form is submitted."
- "Opportunity can't reach Negotiation until the MSA Checklist form is submitted."
See Stage Ownership, Lead Settings, Opportunity Settings.
Targets — Per-Target Attribution Mode
Targets used to always credit the current owner. That broke SDR→AE handoffs: the SDR who qualified the lead lost the credit the moment the AE took ownership.
Each target now has an Attribution block with four modes:
| Mode | Credits | Picks |
|---|---|---|
| Current owner (default) | The entity's owner_id at compute time. | Unchanged behaviour. |
| Stage owner | Whoever owned the entity when it entered the configured stage (via record_stage_assignments). | Requires a stage pick. |
| Stage mover | Whoever moved the entity into the configured stage (via lead_stage_history / opportunity_stage_history). | Requires a stage pick. |
| Creator | created_by on the entity. | n/a |
Activity-based metrics (lead_*, opp_*, calls, meetings, tasks) still always credit performed_by — attribution mode applies only to record-level metrics where ownership is ambiguous.
Use case: a "Qualified Leads by SDR" target = metric: leads_converted, attribution: stage_owner, stageId: <Demo>. Only converted leads count, each attributed to the SDR who owned it when it entered Demo — typically before the handoff.
See Targets Setup.
RBAC — admin / settings module split + view-only mode
A real fix series, not just a rename. Previously non-admin roles with admin.edit = true were 403'd from every @AdminOnly() endpoint (the guard only let through role === 'admin' or roleLevel >= 100), so a "Head of Customer Success" at level 80 couldn't reach general settings, lead/opp settings, CS settings, email templates, approval rules, workflows, API keys, etc. The fix went through three iterations:
- Backend guard fix —
@AdminOnly()is now HTTP-method-aware:GET: requiresadmin.view | admin.edit | settings.view | settings.edit.- Write methods: requires
admin.edit | settings.edit. - Sysadmin (
role === 'admin'or level ≥ 100) always bypasses.
- Frontend
isAdmin+ sidebar —isAdminis reverted to its pure sysadmin meaning so the semantic stays clear. Module access is checked viacanView('settings')/canView('admin'). The sidebar's Administration block now gates each item on its own module permission (Users / Departments / Teams →users.view, Roles →roles.view, Settings →settings.view OR admin.view). The Header user-menu Settings link gates the same way. - View-only mode for users without
settings.edit—AdminLayoutnow detects this combo and:- Shows an amber banner at the top of every settings page explaining the view-only state.
- Wraps the
<Outlet>in a<fieldset disabled>so every native<button>,<input>,<select>, and<textarea>inside the settings pages is disabled by the browser. Zero per-page changes required.
Trade-off: in-page tab switchers and filter dropdowns are also disabled for view-only users. Acceptable because the banner explains why.
Users with active sessions must re-login to pick up the change — permissions live in the JWT payload.
See Roles & Permissions, Users Management.
Index of changes
0300011migration 081 — qualification score columns + per-option scoring77f6f03qualification scoring service + workflow triggerecbfb61per-option score CRUD endpoints + workflow column whitelistsff1b10efrontend API + workflow trigger type + system fields9ae66c8per-option score editor in lead Qualification tabfa37d5fshow qualification score on detail + list + reports0184ee7audit changes shape in updateQualificationFieldebf2f9fauto-rescore on changeStage so qualification updates aren't stale367944aform submission as required-at-stage requirement630451bmigration 083 — enrich stage_owner_type + team round-robin tracking3d5eef9stage ownership resolver supports team_round_robin + role(+team scope)2cb5587enriched stage ownership editor in lead + opportunity settingsa08c26cper-target attribution mode411cfaf@AdminOnly()honorsadmin.edit20effb3frontendisAdminhonorsadmin.edit866f046@AdminOnly()honorssettings/adminview+edit, sidebar gates per-module818cc6eread-only mode for users withoutsettings.edit