Skip to content

Pre-Onboarding

Once a candidate accepts an offer (moved to "Hired" in Ashby), they enter the pre-onboarding phase. This phase bridges the hiring decision with the employee's first day by collecting personal information, creating their BambooHR profile, generating an employment contract, and sending it for signature via DocuSign.

Architecture overview

flowchart LR
    subgraph Inputs
        OF[Onboarding Form\nGoogle Sheets]
        CO[CandidateOffer\nDB]
    end
    subgraph Pre-onboard command
        Match[Match by email]
        Bamboo[Create BambooHR\nemployee]
        Contract[Generate contract\nGoogle Docs]
        Upload[Upload documents\nGoogle Drive]
    end
    subgraph Signature
        DS[DocuSign\nenvelope]
        WH[DocuSign webhook]
        Archive[Archive to\nGoogle Drive]
    end
    OF --> Match
    CO --> Match
    Match --> Bamboo
    Bamboo --> Contract
    Contract --> Upload
    Contract --> DS
    DS --> WH
    WH --> Archive
Hold "Alt" / "Option" to enable pan & zoom

Key files:

File Role
people_ops/commands/pre_onboarding.py CLI command and all pre-onboarding business logic
people_ops/business_logic/onboarding_form.py OnboardingFormResult dataclass, reads from Google Sheets
helpers/bamboohr.py BambooHR API client and field mapping
helpers/docusign.py DocuSign API client (JWT auth)
webhooks/docusign.py DocuSign webhook handler for envelope status updates
alan_home/models/alaner_profile.py AlanerProfile model (drive directories, contract document IDs)
alan_home/models/docusign_envelope.py DocusignEnvelope model
alan_home/models/signed_document.py SignedDocument model

End-to-end flow

flowchart TD
    A["/onboarding_pre_onboard\nSlack command or cron"] --> B["Fetch pending CandidateOffers\n(accepted, approved, sent,\nnot yet pre-onboarded)"]
    B --> C{For each offer}
    C --> D{Offer accepted?}
    D -->|No| E["Post status\n(not accepted yet)\n+ Expire/Refuse buttons"]
    D -->|Yes| F{Returning Alaner?}
    F -->|Yes| G["Mark pre_onboarded_at\nAsk HR to update\nBambooHR manually"]
    F -->|No| H{Form match\nby email?}
    H -->|No| I["Post status\n(form not filled)\n+ Force match dropdown"]
    H -->|Yes| J["_pre_onboard_newcomer()"]
    J --> K["1. Create BambooHR employee"]
    K --> L["2. Create AlanerProfile\nwith restricted Drive dir"]
    L --> M{"Permanent\ncontract?"}
    M -->|Yes| N["3. Generate contract\n(Google Docs)"]
    M -->|No| O["Skip contract"]
    N --> P["4. Upload documents\nto Drive"]
    O --> P
    P --> Q["Post success to Slack\nwith BambooHR + Ashby links"]
Hold "Alt" / "Option" to enable pan & zoom

Onboarding form

The onboarding form is a Google Sheets tab ("Onboarding form") in the salary spreadsheet. Candidates fill it after accepting the offer. The OnboardingFormResult dataclass captures all fields.

Fields collected

Category Fields
Personal First name, last name, preferred name, gender, birthdate, birthplace (city/country), nationality, SSN
Address Street number, street name, post code, province, city, country
Employment Start date, office, work setup (remote/hybrid/on-site)
Banking IBAN, BIC, bank institution, branch, account
FR-specific Disabled worker status, dependent children count
BE-specific Native language, marital status, income partner, dependent children, other dependents
ES-specific ID number (DNI/NIE)
Education Diploma name, school name, diploma URL
CA-specific SIN card/confirmation letter URL, TD1 (federal + provincial) URL

Matching logic

Candidates are matched to CandidateOffer records by email. If no match is found, HR can manually force-match via a Slack dropdown.

BambooHR integration

The _onboard_on_bamboohr() function creates a new employee in BambooHR.

Process

  1. Fetch all employee numbers from BambooHR to determine the next ID (max + 1)
  2. Build the employee payload (50+ fields) from the CandidateOffer and OnboardingFormResult
  3. POST to /employees endpoint
  4. Extract the BambooHR employee ID from the response Location header
  5. Create an AlanerProfile with a restricted Google Drive directory
  6. Set CandidateOffer.pre_onboarded_at and CandidateOffer.alaner_id

Notable field mappings

Computed field Logic
Work email {slugified_first}.{slugified_last}@alan.eu
Professional category Derived from level (A1-C1 → 5, D-F → 6, G-I → 7 for FR/BE; "II"/"I" for ES)
Probation end date FR: 3 months (cat. 5) or 4 months (cat. 6-7). ES: 6 months
Employment status Pattern: [{COUNTRY}] Permanent Full-Time, [{COUNTRY}] Intern, [FR] Apprentice
Compensation Computed via the country-specific compensation scheme (level + experience → salary, equity)
Dependents FR: from fr_dependent_children. BE: be_dependent_children + be_other_dependent

Contract generation

The generate_contract() function creates a personalized employment contract from a Google Docs template. It is called:

  • Automatically by the pre_onboard command for permanent contracts
  • Manually via the Slack action open_generate_contract_modal, which lets HR select a BambooHR employee

Process

  1. Fetch employee data from BambooHR
  2. Select the appropriate contract template based on country and contract type
  3. Copy the template to the employee's restricted Google Drive directory (0. Contract/)
  4. Render template variables (name, address, salary, level, legal entity, etc.)
  5. Store the employment_contract_document_id in AlanerProfile

Contract templates

Template key Contract type Country
fr Permanent France
es Permanent Spain
ca Permanent Canada
be_fr Permanent Belgium (French)
be_nl Permanent Belgium (Dutch)
apprenticeship Apprenticeship France
internship Internship All

Template selection for Belgium uses the candidate's native language (be_native_language from the onboarding form).

Template variables

The contract template is rendered with variables including:

  • FULL_NAME, ADDRESS, START_DATE, ROLE, LEVEL
  • YEARLY_SALARY, MONTHLY_SALARY, CATEGORY (professional category)
  • IBAN, NATIONALITY, SSN, BIRTH_DATE
  • LEGAL_ENTITY, LEGAL_ENTITY_DESCRIPTION, LEGAL_REPRESENTATIVE_TITLE
  • JOB_DESCRIPTION_{FR,EN,ES} (from a gsheet via Turing)
  • FR_PROBATION_PERIOD, ES_EXCLUSIVITY_AGREEMENT
Entity Description
Alan SA Main French entity
Alan Insurance French regulated insurance entity
Alan Services French services entity
Alan Tech French tech entity
Marmot BE Belgian entity
Marmot Iberia Spanish entity
Alan CA Inc. Canadian entity

DocuSign integration

Sending for signature

The send_contract_for_signature() function creates a DocuSign envelope with two documents. It is called:

  • Manually via the Slack action open_send_contract_modal, which lets HR select a BambooHR employee and an HR signer (filtered by eu_tools_manage_hr permission)
flowchart LR
    subgraph Envelope
        D1["Employment contract\n(from Google Docs → PDF)"]
        D2["Acceptable Use Policy\n(DocuSign template)"]
    end
    subgraph Signers
        S1["1. Newcomer\n(employee)"]
        S2["2. People team\n(HR signer)"]
        S3["3. Newcomer CC\n(carbon copy)"]
    end
    Envelope --> S1
    Envelope --> S2
    D1 --> S3
Hold "Alt" / "Option" to enable pan & zoom

Process:

  1. Export the Google Docs contract to PDF
  2. Create a composite DocuSign envelope:
    • Document 1: Employment contract (uploaded PDF + country-specific server template for signature fields)
    • Document 2: Acceptable Use Policy (server template, newcomer-signed only)
  3. Configure recipients: newcomer (signer), People team member (signer), newcomer (CC)
  4. Envelope is created in "created" status (HR reviews before sending)
  5. Store DocusignEnvelope and SignedDocument records in the database

The email subject follows the pattern: "{NAME} @ Alan {FLAG} - Your employment contract ✨"

DocuSign webhook

The POST /webhooks/docusign endpoint handles envelope lifecycle events:

stateDiagram-v2
    [*] --> Created: send_contract_for_signature()
    Created --> Sent: envelope-sent
    Sent --> Delivered: envelope-delivered
    Delivered --> Completed: envelope-completed
    Delivered --> Declined: envelope-declined
    Created --> Voided: envelope-voided

    Completed --> Archived: Documents archived\nto Google Drive
Hold "Alt" / "Option" to enable pan & zoom
Event Action
envelope-sent Sets DocusignEnvelope.sent_at
envelope-delivered Sets delivered_at
envelope-completed Sets signed_at, downloads archive, uploads to employee's restricted Drive directory
envelope-voided Sets voided_at
envelope-declined Sets declined_at

Authentication: SHA-256 base64 signature in x-docusign-signature-1 header.

Slack integration

Trigger

The pre-onboarding process is triggered by the /onboarding_pre_onboard Slack command (runs in #team_hr, or #eng_sandbox in dry-run mode).

Thread structure

The command posts a summary thread tagging @alaner_onboarding_crew:

  • Header: count of pending offers and breakdown
  • Per-offer messages with status:
    • :check: Pre-onboarded successfully (with BambooHR + Ashby links)
    • :yellowprogress: Form not filled (with force-match dropdown)
    • :paused-red: Not accepted yet (with Expire/Refuse buttons)
    • :warning: Error (with Datadog log explorer link)

Interactive actions

Action Handler Description
pre_onboarding_expire_offer expire_offer() Mark offer as expired
pre_onboarding_refuse_offer refuse_offer() Mark offer as refused
pre_onboarding_force_matching force_matching() Manually match offer to form result
open_generate_contract_modal generate_contract() Open modal to select a BambooHR employee and generate their contract
open_send_contract_modal send_contract_for_signature() Open modal to select employee + HR signer, creates DocuSign envelope

Data model

erDiagram
    CandidateOffer ||--o| Alaner : "owner (recruiter)"
    CandidateOffer ||--o| Alaner : "alaner (employee)"
    CandidateOffer ||--o| Alaner : "approver"
    AlanerProfile ||--|| Alaner : "alaner_id"
    AlanerProfile ||--o{ SignedDocument : "signed_documents"
    SignedDocument }o--|| DocusignEnvelope : "docusign_envelope"

    CandidateOffer {
        uuid application_id
        uuid candidate_id
        int owner_id
        int alaner_id
        datetime pre_onboarded_at
    }
    AlanerProfile {
        int alaner_id
        string restricted_drive_directory_id
        string employment_contract_document_id
    }
    SignedDocument {
        enum type "employment_contract | acceptable_use_policy"
    }
    DocusignEnvelope {
        string docusign_envelope_id
        datetime sent_at
        datetime delivered_at
        datetime signed_at
        datetime voided_at
        datetime declined_at
    }
Hold "Alt" / "Option" to enable pan & zoom