Skip to content

Reviews

Overview

The Reviews system handles peer reviews, self-reviews, and continuous feedback. It supports:

  • Cycle-based reviews: fixed-date cycles (e.g. biannual performance reviews) or lifecycle cycles (e.g. newcomer reviews triggered by tenure)
  • Ad-hoc reviews: one-off reviews not tied to a cycle
  • Continuous feedback: lightweight single-question reviews, imported from Lattice or Slack
  • Self-reviews: optional self-assessment as part of a review cycle
  • ABAC-gated access: fine-grained policies controlling who can view, submit, approve, and release reviews
  • PDF export & Google Drive release: completed reviews are exported to PDF and uploaded to the alaner's Drive folder

Data model

ReviewTemplate

Defines the set of questions used in a review cycle.

Field Type Description
name Text Template name
description Text Template description
questions JSONB[list[ReviewQuestion]] Array of question dataclasses

Question types (ReviewQuestionType): text, select, checkbox, callout

Each ReviewQuestion has:

Field Type Description
id str Unique question identifier
type ReviewQuestionType One of the four types above
title str? Question title
description str? Question description / helper text
options dict? Type-specific options (e.g. values: list[OptionValue] for select)

Relationships: review_cycles (cycles using this as main template), self_review_cycles (cycles using this as self-review template)

ReviewCycle

Groups reviews into a campaign. Determines who gets reviewed, when, and with what template.

Field Type Description
name Text Cycle name
description Text Cycle description
type ReviewCycleType fixed_date or lifecycle
template_id UUID (FK) Main review template
self_review_template_id UUID? (FK) Optional self-review template
start_date Date? Start date (None for lifecycle cycles)
duration Integer Duration in weeks
is_cancelled Boolean Whether the cycle has been cancelled
reviewees_smart_group_id UUID (FK) Smart group determining who gets reviewed
min_reviewers Integer Minimum number of reviewers required
max_reviewers Integer Maximum number of reviewers allowed

Hybrid property: is_active - True when not cancelled AND (lifecycle OR start_date <= today < start_date + duration weeks)

Relationships: template, self_review_template, reviewees_smart_group, smart_reviewees (alaners in the smart group on cycle start date), reviews

Review

A single review for one alaner. Extends Historizable (provides start_date, end_date).

Field Type Description
name Text Review name
alaner_id Integer The reviewee
type ReviewType feedback, review_cycle, or ad_hoc
review_cycle_id UUID? (FK) Associated cycle (nullable for feedback/ad-hoc)
questions JSONB[list[ReviewQuestion]] Questions for reviewers (copied from template)
self_review_questions JSONB[list[ReviewQuestion]] Questions for self-review (copied from template)
min_reviewers Integer Minimum reviewers required
max_reviewers Integer Maximum reviewers allowed
status ReviewStatus Computed status (see state machine below)
released_on Date? Date when review was released to the alaner

Relationships: alaner, review_cycle, review_submissions

ReviewSubmission

A single reviewer's submission for a review. Extends Historizable (provides start_date, end_date).

Field Type Description
review_id UUID (FK) Parent review
reviewer_id Integer The reviewer
answers JSONB[dict]? Reviewer's answers (data-privacy protected via released_on)
type ReviewSubmissionType self, coach, coachee, or peer
approved_at DateTime? When the submission was approved by the coach
approver_id Integer? Who approved
rejected_at DateTime? When the submission was rejected
rejection_reason Text? Reason for rejection
rejecter_id Integer? Who rejected
published_at DateTime? When the submission was published
permalink Text? Permanent link to the submission
released_on Date? Date when released (controls data privacy retention)

Relationships: review, reviewer, approver, rejecter

Review status machine

Review status is stored on the Review model and recomputed by compute_review_status() after each status-changing operation (submission created, approved, rejected, published).

flowchart TD
    A[Start] --> B{Non-self submissions<br>< min_reviewers?}
    B -->|Yes| C[ready_for_reviewers_selection]
    B -->|No| D{Approved non-rejected<br>< min_reviewers?}
    D -->|Yes| E[ready_for_reviewers_approval]
    D -->|No| F{review_cycle or ad_hoc<br>AND no published self-review?}
    F -->|Yes| G[ready_for_self_review]
    F -->|No| H{All non-rejected<br>submissions published?}
    H -->|Yes| I[completed]
    H -->|No| J[ready_for_submissions]
Hold "Alt" / "Option" to enable pan & zoom
Status Condition
ready_for_reviewers_selection Fewer non-self submissions than min_reviewers
ready_for_reviewers_approval Not enough approved (non-rejected) submissions to meet min_reviewers
ready_for_self_review Review is review_cycle or ad_hoc type and no self-review has been published yet
ready_for_submissions Waiting for remaining reviewers to publish
completed All non-rejected submissions are published

Review cycle types

fixed_date

Has a concrete start_date. Active while start_date <= today < start_date + duration weeks. Used for scheduled campaigns like biannual performance reviews.

lifecycle

start_date is None, making the cycle always active. Used for reviews triggered by tenure milestones (e.g. newcomer reviews at 2.5 months and 5 months).

Submission lifecycle

sequenceDiagram
    participant Reviewee as Reviewee
    participant Coach as Coach / Manager
    participant Reviewer as Reviewer
    participant System as System

    Note over System: Review created<br>(status: ready_for_reviewers_selection)

    Reviewee->>System: POST submission (add reviewer)
    Note over System: ReviewSubmissionCreated event
    Note over Reviewee,Coach: Coach can also add reviewers<br>as backup

    alt Enough reviewers added
        Note over System: status: ready_for_reviewers_approval
    end

    Coach->>System: PUT validation (approve)
    Note over System: ReviewSubmissionApproved event

    alt Enough approved
        Note over System: status: ready_for_self_review<br>(if self-review needed)
    end

    alt Submission rejected
        Coach->>System: DELETE validation (reject)
        Note over System: ReviewSubmissionRejected event
    end

    Reviewer->>System: PATCH answers
    Reviewer->>System: PUT publication
    Note over System: ReviewSubmissionPublished event

    alt All published
        Note over System: status: completed
    end
Hold "Alt" / "Option" to enable pan & zoom

Continuous feedback

Continuous feedback is stored as Review with type=feedback, a single question, and auto-completed status.

Sources

  • Lattice import: flask reviews import_feedback_from_lattice imports historical feedback from Lattice via Turing
  • Slack import: flask reviews import_feedback_from_slack converts SlackMessage records (type continuous_feedback) into feedback reviews

Slack workflow

Messages posted in the #continuous-feedback Slack channel are parsed by flask slack import_feedback and stored as SlackMessage records. These are then imported as feedback reviews via the command above.

When feedback is imported, a thread reply is posted pinging the receiver's coach for visibility.

AI summary

POST /alaners/<id>/feedback-summary aggregates received feedback over a configurable period (6-12 months) and sends it to Dust for AI-powered analysis. Returns a 204 with a Location header pointing to the Dust conversation.

User lifecycle integration

ReviewsProvider and ReviewStartTask handle automatic review creation:

  1. ReviewsProvider.get_all_users() returns active review cycles per alaner email (cached 20 min)
  2. ReviewStartTask.should_run() checks if the alaner is active and has at least one active cycle without a review
  3. ReviewsProvider.provision_user():
    • For each active ReviewCycle, checks if alaner is in reviewees_smart_group but has no Review yet
    • Creates Review with questions copied from the cycle's template
    • Auto-creates a self-review ReviewSubmission
    • Publishes ReviewCreated webhook event

ABAC policies

Access control is defined in access_policies/people.py:

Policy ID Description
RevieweePolicy reviewee Allows actions on your own review (review.alaner_id == principal.id)
ReviewerPolicy reviewer Allows actions on your own submissions (submission.reviewer_id == principal.id)
PeerReviewerPolicy peer-reviewer Allows viewing published self-review if you're a reviewer on the same review
RevieweeCoachPolicy reviewee-coach Allows coach to manage coachee's review (review.alaner_id in principal.coachees)
ReviewConsultationPolicy review-consultation Allows viewing completed + released review, or if ManageReviewPolicy applies
ManageReviewPolicy (composite) RevieweeCoachPolicy OR eu_tools_manage_hr permission

PDF export and release

export_to_pdf() generates a PDF from all published, non-rejected submissions:

  1. Submissions are sorted by type (self > coach > coachee > peer), then by reviewer last name
  2. Markdown answers are converted to HTML
  3. HTML is rendered to PDF with custom CSS (css/reviews.css)

Release flow (PUT /reviews/<id>/release):

  1. Validates status is completed and type is not feedback
  2. Exports to PDF
  3. Uploads to Google Drive in the alaner's directory under Reviews/ folder
  4. Sets released_on on the review and all submissions

Webhook events

All events extend WebhookMessage and include review + review_cycle (nullable) schemas:

Event Additional fields Trigger
ReviewCreated - New review provisioned by user lifecycle
ReviewStatusUpdated - Status changes (submission added, approved, published, etc.)
ReviewExtended previous_start_date, previous_end_date, start_date, end_date Review dates extended via PATCH
ReviewSubmissionCreated review_submission New submission added
ReviewSubmissionApproved review_submission Submission approved by coach
ReviewSubmissionRejected review_submission Submission rejected by coach
ReviewSubmissionPublished review_submission Submission published by reviewer