AI Interview
1. Dataset Name
ai_interview
2. Description
Interview-level dataset that pivots AI Interviewer event activity from user_analytics into one row per interview session, enriched with position, application, feedback, and credit-plan details. Each row represents a single AI interview, with milestone timestamps (invited, call started, completed, feedback generated/reviewed), derived durations, and recruiter/sourcer/hiring-manager ownership denormalized onto it. Also includes fuller position context (business unit, department, location, ATS job ID), AI-assistance request detail (assistance_source), and — for modular interviews specifically — avatar usage classification and live-section timing parsed from the interview's guide config (modular_interview_type, has_avatar, live section counts/bounds). Designed to power adoption, speed/efficiency, candidate experience, credit consumption, and modular-interview (avatar vs. non-avatar) analyses for the AI Interviewer dashboard.
3. Source Tables
- customer_views.user_analytics (pivoted per interview_id into milestone timestamps and AI-assistance requests)
- customer_views.interview_session (interview-level record — status, credits_used, interview_survey_rating, data_json guide config for modular interviews)
- customer_views.positions (recruiter, hiring manager, business unit, department (job function), location, status, creation timestamp, ATS job ID)
- customer_views.applications (match score and offer/hire outcome)
- customer_views.profile_feedback (sourcer attribution)
- customer_views.ai_credit_purchased_data (pre-aggregated into a total_credits_purchased CTE, joined via group_id)
- customer_views.employee (joined twice, via a deduped emp CTE, to resolve recruiter and hiring manager display names)
- customer_views.ai_interview_event_log (section_started / section_completed events, used to bound live sections for modular interviews)
4. Grain
One row per interview_id. Built from a left-join chain starting at the pivoted event CTE, so an interview with no completion, feedback, or application match still appears as a row with NULLs in the corresponding fields. The pivoted event CTE only contains interviews with at least one of the seven tracked interview_session_* events — an interview with zero tracked events never appears in the dataset at all, even if the interview_session record itself exists. A final DISTINCT is applied in the _ef_dataset CTE, so duplicate joins (e.g. multiple application matches) are collapsed.
5. Dashboards Supported
- AI Interviewer (AII) Dashboard
- 360 Interview (Modular Interview) Dashboard
6. Metrics Supported
| Metric | Supported Breakdowns |
| AI Interview Invites Sent | interview_type_label, recruiter, sourcer |
| AI Interviews Completed | interview_type_label, recruiter, sourcer |
| Interview Count by Feedback Turnaround Time Bucket | interview_type_label, recruiter, sourcer |
| Average Interview Time | recruiter, interview_type_label, sourcer |
| Candidate Satisfaction Score | interview_type_label, recruiter, sourcer |
| Human Assistance Requested | interview_type_label, recruiter, sourcer |
| Interviews with Retry/Rejoin | interview_type_label, recruiter, sourcer |
| Interviews with Call Initiation Error | interview_type_label, recruiter, sourcer |
| Weekly Trend of Interviews Completed | interview_type_label, recruiter, sourcer |
| AI Interview Completed — Multi vs Single Section | sourcer, recruiter |
7. Known Exclusions / Filters Applied
- Only the seven interview_session_* events are pivoted from user_analytics; any other event types on the same entity_id are ignored.
- Interviews with out-of-order event sequences are dropped from the dataset entirely, not just nulled — an interview's pivoted row is removed if call_started_at exists without invited_at, completed_at exists without call_started_at, feedback_generated_at exists without completed_at, or feedback_reviewed_at exists without feedback_generated_at. This is a data-quality guard, not a business filter, and can silently remove otherwise-valid interviews if upstream tracking is incomplete.
- completed_at uses MIN(timestamp) of the completion event (first completion), while completed_at_max uses MAX(timestamp) — used specifically for feedback turnaround calculations to anchor off the latest completion signal.
- duration_minutes is only populated when completed_at > call_started_at and the difference is positive — invalid or out-of-order timestamps resolve to NULL rather than a negative duration.
- is_call_initiation_error = TRUE only when a call was initiated but never reached the in-progress state (call_initiated_at IS NOT NULL AND call_started_at IS NULL).
- total_credits_purchased is pre-aggregated as SUM(total_credits) from ai_credit_purchased_data grouped by group_id, then joined onto every interview row for that customer — the value is denormalized and repeats across rows, so MAX() (not SUM) must be used when aggregating it in downstream metrics.
- sourcer defaults to 'Unassigned' via COALESCE when no owning user can be resolved.
- recruiter and hiring_manager are both resolved to "First Last (email-prefix)" via a deduped emp CTE joined against customer_views.employee; when no employee match is found, they fall back to the raw email, then to 'Unassigned'.
- interview_type_label is derived through a CASE waterfall over interview_type (profile_screening → Screening, coding_interview → Coding, behavioural_interview → Functional/Behavioural, modular_interview → Modular); any unmapped value falls to 'Other'.
- The final SELECT applies DISTINCT in the _ef_dataset CTE, so duplicate joins (e.g. multiple application matches) are collapsed.
- modular_interview_type, has_avatar, and modular_interview_type_credits are populated only for interview_type = 'modular_interview'; null for all other interview types. Both are derived as a proxy off credits_used (1.5/4.5 = avatar, 1/3 = no avatar, everything else falls to null/'Unclassified') rather than an explicit flag — treat as an approximation, not ground truth.
- live_section_count, first_live_section_start, last_live_section_end_effective are derived from the interview's data_json guide config and ai_interview_event_log, effectively populated only for modular interviews; "live" sections explicitly exclude pre_screening and assessment section types.
- business_unit, department, location_country, ats_job_id are null if position_id doesn't join to a positions record.
8. Schema
| Column Name | Data Type | Notes |
| interview_id | bigint | Primary grain key for this dataset; sourced from interview_session.id / user_analytics.entity_id |
| profile_id | bigint | Candidate's profile identifier |
| position_id | bigint | Join key to the position the interview is tied to |
| created_at | datetime | Interview record creation timestamp |
| interview_type | varchar | Raw interview type from interview_session |
| interview_type_label | varchar | Derived: mapped label (Screening, Coding, Functional/Behavioural, Modular, Other) via CASE WHEN waterfall over interview_type |
| invited_at | datetime | MAX(timestamp) WHERE event = interview_session_candidate_contacted, sourced from user_analytics |
| call_initiated_at | datetime | MIN(timestamp) WHERE event = interview_session_call_initiated, sourced from user_analytics |
| call_started_at | datetime | MIN(timestamp) WHERE event = interview_session_call_in_progress, sourced from user_analytics |
| completed_at | datetime | MIN(timestamp) WHERE event = interview_session_interview_completed, sourced from user_analytics; first completion signal |
| completed_at_max | datetime | MAX(timestamp) WHERE event = interview_session_interview_completed; used as the anchor for feedback turnaround |
| completed_at_week | datetime | Derived: DATE_TRUNC('week', completed_at) |
| completed_at_month | datetime | Derived: DATE_TRUNC('month', completed_at) |
| feedback_generated_at | datetime | MAX(timestamp) WHERE event = interview_session_feedback_generated, sourced from user_analytics |
| feedback_reviewed_at | datetime | MAX(timestamp) WHERE event = interview_session_feedback_reviewed, sourced from user_analytics |
| is_invited | bigint | Flag (0/1); 1 if invited_at is not null |
| is_call_initiated | bigint | Flag (0/1); 1 if call_initiated_at is not null |
| is_call_started | bigint | Flag (0/1); 1 if call_started_at is not null |
| is_completed | bigint | Flag (0/1); 1 if completed_at is not null |
| is_feedback_generated | bigint | Flag (0/1); 1 if feedback_generated_at is not null |
| is_feedback_reviewed | bigint | Flag (0/1); 1 if feedback_reviewed_at is not null |
| is_call_initiation_error | bigint | Derived: 1 if call_initiated_at IS NOT NULL AND call_started_at IS NULL |
| is_assistance_requested | bigint | Flag (0/1); 1 if event = interview_session_ai_candidate_requested_assistance occurred |
| duration_minutes | bigint | Derived: TIMESTAMPDIFF(MINUTE, call_started_at, completed_at), only when positive and both timestamps present |
| minutes_to_feedback | bigint | Derived: TIMESTAMPDIFF(MINUTE, completed_at_max, feedback_generated_at), only when feedback follows completion |
| feedback_time_bucket | varchar | Derived bucket of minutes_to_feedback: Within 5 mins, 5-15 mins, 15-30 mins, 30-60 mins, 1-4 hours, 4+ hours |
| survey_rating | double | Candidate's interview survey rating; sourced from interview_session |
| has_rating | bigint | Flag (0/1); 1 if survey_rating is not null |
| sourcer | varchar | COALESCE(requester_name, 'Unassigned'); sourced from profile_feedback |
| recruiter | varchar | Resolved to "First Last (email-prefix)" via a deduped emp CTE joined to customer_views.employee; falls back to recruiter_name/raw recruiter email/'Unassigned' if no match |
| is_open | bigint | Flag (0/1) from source position record; sourced from positions |
| status | varchar | Raw status of the position; sourced from positions |
| creation_ts | datetime | Position creation timestamp; sourced from positions |
| ai_enabled | bigint | Derived: TRUE if position_id is not null on the interview record |
| time_to_interview_hrs | decimal | Derived: ROUND(TIMESTAMPDIFF(MINUTE, invited_at, completed_at) / 60.0, 1), only when completion follows invite |
| feedback_review_time_hrs | bigint | Derived: TIMESTAMPDIFF(HOUR, feedback_generated_at, feedback_reviewed_at), only when review follows generation |
| interview_status | varchar | Raw status of the interview; sourced from interview_session |
| link_expiry_timestamp | bigint | Expiry timestamp of the interview invite link; sourced from interview_session |
| call_session_count | bigint | Number of call sessions for the interview; sourced from interview_session; >1 indicates retry/rejoin |
| credits_used | double | Credits consumed by the interview; sourced from interview_session |
| total_credits_purchased | double | Derived: SUM(total_credits) grouped by group_id, sourced from ai_credit_purchased_data, joined onto every interview row for the customer (denormalized — use MAX(), not SUM(), downstream) |
| match_score | double | Candidate's Eightfold match score for the position; sourced from applications |
| offered_at | datetime | Offer timestamp; sourced from applications |
| hired_at | datetime | Hire timestamp; sourced from applications |
| is_offered | bigint | Flag (0/1); 1 if offered_at is not null |
| is_hired | bigint | Flag (0/1); 1 if hired_at is not null |
| group_id | varchar | Tenant/customer scoping |
| assistance_source | varchar | Derived: bucketed reason for the assistance request (5 known reasons, e.g. "I want to talk to a human recruiter"); anything else rolls up to 'Others'; null if no assistance requested |
| recruiter_email | varchar | Raw recruiter email from the source position record |
| hiring_manager_email | varchar | Raw hiring manager email from the source position record |
| hiring_manager | varchar | Resolved the same way as recruiter, keyed off hiring_manager_email; falls back to 'Unassigned' |
| business_unit | varchar | Position's business unit, sourced from positions |
| department | varchar | Position's job function, aliased as department, sourced from positions |
| location_country | varchar | Position location, country, sourced from positions |
| ats_job_id | varchar | External ATS job identifier for the position, sourced from positions |
| modular_interview_type | varchar | Modular interviews only: "More than one live section" if credits_used >= 3, "Exactly one live section" if credits_used <= 1.5, else "Unclassified"; null for non-modular interviews |
| has_avatar | boolean | Modular interviews only: TRUE if credits_used is 1.5 or 4.5; else FALSE; null for non-modular interviews. Proxy off credits_used, not an explicit flag |
| modular_interview_type_credits | varchar | Modular interviews only: finer-grained label combining session count and avatar usage based on exact credits_used value (1, 1.5, 3, 4.5); null otherwise |
| live_section_count | bigint | Count of "live" guide sections (excludes pre_screening/assessment) parsed from the interview's data_json; effectively modular-interview only |
| first_live_section_start | datetime | Timestamp the first live section started for that interview — MIN(section_started) across the interview's live sections, from ai_interview_event_log |
| last_live_section_end_effective | datetime | Timestamp the last live section ended for that interview — MAX(section_completed) across the interview's live sections, from ai_interview_event_log |