Skip to main content

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.

Migrations to apply

Two tenant migrations ship with this release. Run them on prod with:

npx ts-node apps/api/src/scripts/run-tenant-migrations.ts
MigrationAdds
081leads.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.
083Drops 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] and field weights sum to 100. The active framework header shows the live total vs the required 100.
  • qualificationScore + qualificationScoreBreakdown computed 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 changechangeStage now 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_score is now a data-source field on leads. Filter, group, and aggregate on it.
  • New workflow triggerlead_qualification_score_changed fires whenever the value transitions; independent from lead_score_changed so each engine has its own automation hooks.
  • System fields registeredsystem-score and system-qualificationScore are now non-editable computed fields in the registry, so they appear in workflow condition pickers, page designer, and other registry consumers.
  • qualification_score added to update_field allowlist (for workflow update_field actions) 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

ModePicks viaCursor
inheritFalls through to record owner.n/a
userSingle specific user (with an optional team filter that narrows the user dropdown — UX only, doesn't change resolution).n/a
team_leadThe 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_idteam_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:

ModeCreditsPicks
Current owner (default)The entity's owner_id at compute time.Unchanged behaviour.
Stage ownerWhoever owned the entity when it entered the configured stage (via record_stage_assignments).Requires a stage pick.
Stage moverWhoever moved the entity into the configured stage (via lead_stage_history / opportunity_stage_history).Requires a stage pick.
Creatorcreated_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:

  1. Backend guard fix@AdminOnly() is now HTTP-method-aware:
    • GET: requires admin.view | admin.edit | settings.view | settings.edit.
    • Write methods: requires admin.edit | settings.edit.
    • Sysadmin (role === 'admin' or level ≥ 100) always bypasses.
  2. Frontend isAdmin + sidebarisAdmin is reverted to its pure sysadmin meaning so the semantic stays clear. Module access is checked via canView('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.
  3. View-only mode for users without settings.editAdminLayout now 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.

After deploying

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

  • 0300011 migration 081 — qualification score columns + per-option scoring
  • 77f6f03 qualification scoring service + workflow trigger
  • ecbfb61 per-option score CRUD endpoints + workflow column whitelists
  • ff1b10e frontend API + workflow trigger type + system fields
  • 9ae66c8 per-option score editor in lead Qualification tab
  • fa37d5f show qualification score on detail + list + reports
  • 0184ee7 audit changes shape in updateQualificationField
  • ebf2f9f auto-rescore on changeStage so qualification updates aren't stale
  • 367944a form submission as required-at-stage requirement
  • 630451b migration 083 — enrich stage_owner_type + team round-robin tracking
  • 3d5eef9 stage ownership resolver supports team_round_robin + role(+team scope)
  • 2cb5587 enriched stage ownership editor in lead + opportunity settings
  • a08c26c per-target attribution mode
  • 411cfaf @AdminOnly() honors admin.edit
  • 20effb3 frontend isAdmin honors admin.edit
  • 866f046 @AdminOnly() honors settings/admin view+edit, sidebar gates per-module
  • 818cc6e read-only mode for users without settings.edit