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
- Speaker matching (
getHubMemberDetails.js,addEmailsToTranscript.js): transcript speaker labels are matched to Hubusers/user_emails, fuzzy-matching unresolved speakers against the calendar invitee/participant lists forwarded by thehelixrepo. - Sentence grouping: consecutive sentences from the same speaker are grouped for better LLM performance.
helix_meetingrow created/updated: transcript, model, temperature,reasoning_effort, and the raw LLM response (raw_analysis) are stored, withhelix_meeting_skillrows linking which skills were assessed.- 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). - 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,nullif none recorded) is always computed and stored here regardless of whether the domain'shelixShowOverallScoreflag is on — the flag only gates whether it's returned/rendered (see Admin config). - Email sent: a "meeting analysis" templated email per participant, showing top/bottom skill, respecting the domain's
helixUIModeandhelixSendAnalysisEmailssettings (see Admin config), with an optional 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. API and MCP responses that return a score also return a scoreOutOf field alongside it; consumers should always read that rather than assuming a 0–100 or "out of 5" scale.
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'.
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.