Skip to main content

Scoring engine

Once a meeting transcript lands on HELIX_SQS_QUEUE_URL (see Architecture), api/modules/helixProcessing/processHelixData.js — the heart of Helix in Hub — turns it into per-user, per-skill scores. This page covers that pipeline, plus the weekly roll-up and manual reprocessing.

From transcript to stored scores​

  1. Speaker matching (getHubMemberDetails.js, addEmailsToTranscript.js): transcript speaker labels are matched to Hub users/user_emails, fuzzy-matching unresolved speakers against the calendar invitee/participant lists forwarded by the helix repo.
  2. Sentence grouping: consecutive sentences from the same speaker are grouped for better LLM performance.
  3. helix_meeting row created/updated: transcript, model, temperature, reasoning_effort, and the raw LLM response (raw_analysis) are stored, with helix_meeting_skill rows linking which skills were assessed.
  4. Per-user analysis via analyseMeetingTranscript() (api/modules/helixProcessing/prompt.js), which calls OpenAI (gptMessage, default model configurable, e.g. gpt-5) once per user, using one of two response formats/engines (see below).
  5. Results stored: helix_user_analysis (a weekly bucket per user/domain), helix_user_meeting, helix_user_meeting_skill. Reprocessing an existing meeting purges and recreates these dependent rows rather than appending. helix_user_meeting.overall_score (calculateOverallScore() — average of the meeting's skill scores, rounded to 1dp, null if none recorded) is always computed and stored here regardless of the domain's Show overall score settings — they only gate whether it's returned/rendered (see Admin config → Display settings).
  6. Email sent: a "meeting analysis" templated email per participant, showing top/bottom skill, respecting helixSendAnalysisEmails and the user view display settings — emails always use the user view, admins included (see Admin config → Which view applies). formatEmailScore() (api/modules/helixProcessing/utils/index.js) folds the band into the score string — 4/5 (Excellent), or just Excellent when scores are hidden — so the templates need no band fields of their own. The email can also link out to Mindset Coach — a separate AI-coaching feature that Helix links to (getMindsetCoachUrl, api/modules/helixProcessing/utils/index.js).

Which skills get analysed for a given user?​

getSkillPrompts(userIds) (prompt.js) resolves, per user, which helix_skills apply to them: a user's applicable skills come from helix_set rows scoped to their helix_domain_id, either targeting audience: 'all' or audience: 'teams' (joined through helix_set_team against the user's team memberships). Each skill carries its own helix_prompt (the skill-specific prompt text + 3Q weights) and its helix_agent (which in turn has its own prompt — the overall system/developer prompt for the analysis call). See Data model for the full table relationships.

Two scoring engines​

The LLM call's response schema is picked by responseFormat ('3Q' vs. anything else, treated as 'open'), driven by helix_agent.name:

3Q engine​

The LLM (ZodUserAnalysis3Q schema) returns, per skill, a list of positive/negative "skill usage" phrases, each with a contextScore and sentimentScore. get3QSkillsWithScores() then computes a weighted score in code — this is the actual "3Q Engine" the product page refers to (quantitative key-term count, qualitative context, sentiment):

// api/modules/helixProcessing/processHelixData.js
const raw_key_count = Math.max(pos_count - neg_count, 0);
const max_term_count_reference = pos_count + neg_count;

const context_indicator = average(contextScore across all usages);
const sentiment_indicator = average(sentimentScore across all usages);
const keyword_indicator = raw_key_count / max_term_count_reference;

const raw_score = keyword_indicator * w1 + context_indicator * w2 + sentiment_indicator * w3;
const score = 100 * raw_score / (w1 + w2 + w3);

w1/w2/w3 (key-term / context / sentiment weights) come from helix_prompt.w1_keyterms / w2_context / w3_sentiment, set per skill-prompt. A code comment (processHelixData.js) flags a known quirk in this formula worth being aware of before changing it: raw_key_count clamps negative differences to zero, so e.g. 1 positive + 1 negative and 1 positive + 2 negative both currently produce the same raw_key_count of 0.

Open engine​

The LLM (ZodUserAnalysisSkillOpen schema) is simply asked to return a score (0–100) directly per skill, alongside the same positive/negative usage phrases and a summary/suggestion, and that score is used as-is. This is a simpler, more directly LLM-judged alternative to 3Q, selected purely by which helix_agent a skill is wired to.

Both engines write to the same helix_user_meeting_skill columns (pos_count, neg_count, context_indicator, sentiment_indicator, raw_key_count, max_term_count_reference, keyword_indicator, raw_score, score) — for the Open engine, the indicator columns are simply left at their defaults since only score is meaningful.

Score scale​

Both engines produce a score on a 0–100 scale internally. analysisPostProcessing() then rescales it to the domain's configured helixScoreOutOf setting (see Admin config) — default 100, i.e. a no-op — before it's stored on helix_user_meeting_skill.score, so all downstream consumers (weekly roll-up, emails, API responses, the mindsetMcp tools) already see scores on the domain's chosen scale. When a response's scores are on the domain's scale, it also returns a scoreOutOf field; consumers should always read that rather than assuming a 0–100 or "out of 5" scale.

The one exception is a view with scores hidden and score bands shown: there the API replaces each score with its band's position (1 = lowest band), sets scoresNormalisedToBands: true, and leaves out scoreOutOf. Check scoresNormalisedToBands before treating a score field as a score — see Admin config → Display modes. The MCP tools strip these positions, so MCP responses only ever hold domain-scale scores.

Summary format​

The summary returned for a meeting/skill is Markdown with three headed sections — Overall assessment, What was done well, Where to improve (the latter two as bullet lists) — rather than a single text blob; the separate suggestion field is untouched. The format instructions are threaded in as a separate message after the transcript rather than mutated into the cached agentPrompt, to preserve OpenAI prompt-cache hits across domains sharing a meeting. A domain can override the instructions via the helixMeetingSummaryFormatPrompt/helixSkillSummaryFormatPrompt settings (see Admin config). The Markdown is rendered to HTML server-side (markdown-it) and shown via the FormattedText component, falling back to plain text when HTML isn't present.

Weekly roll-up​

Independent of the per-meeting flow, weeklyAnalysis.js + sendWeeklyAnalysis.js average each user's scores per skill across the week's meetings, produce a structure-aware summary (summariseStructured(), which preserves the Markdown headings above when merging multiple meeting summaries), and populate helix_user_analysis/helix_user_analysis_skill. It emails a "weekly analysis" template to every Helix-enabled domain — the query filters on domains.config->'$.helixIntegrationMode' IN ('both', 'helix') and helixSendAnalysisEmails != 'false'. Like the meeting email, it uses the user view's display settings and formatEmailScore().

helix_user_analysis.overall_score is the average of that week's per-meeting overall_score values (not a flat average across every skill score recorded that week — that would weight meetings unevenly depending on how many skills each one covered). All averaged scores shown to users or admins — weekly per-skill scores, overall scores, admin analytics averages, emails, and the CSV report — are rounded to the domain's helixScoreDecimalPlaces setting (0/1/2, default 0); individual meeting skill scores stay whole numbers.

This can be triggered manually (outside of any schedule) via POST /api/sendWeeklyAnalysis (api/routes/sendWeeklyAnalysis/post.js), exposed in the Admin Settings page as "Run Helix weekly email for all users on all hubs" (frontend/src/admin/pages/settings/.../RunHelixWeeklyEmail/).

Manual reprocessing​

If a meeting needs to be re-analysed (new prompt, different model, fixed transcript), an admin can re-run this whole pipeline directly, without going back through a bot: POST/PUT /meeting/{id} (api/routes/meeting/{id}/) re-runs processHelixData against an existing or duplicated meeting, optionally overriding model, temperature, reasoning_effort, service_tier, or the prompt/skills/transcript entirely. The Admin app's Meeting Reprocessing page (frontend/src/admin/pages/meetingReprocessing/) is the UI for this, gated to SUPER_USER.