Rye Own your data. Let agents do the rest.

Cookbooks

Cookbook: Recruiting Pipeline

Track candidate progress with full history so hiring decisions are consistent and explainable.

Source file: design/cookbooks/recruiting-pipeline.md

Cookbook: Recruiting Pipeline

Candidates, Interviews, Offers, and Hires


The Problem

A growing tech company tracks candidates in a spreadsheet, interview feedback in Slack threads, and offer details in email. When a hiring manager asks “Where are we on the senior engineer search?”, someone spends 20 minutes piecing it together. When a candidate reapplies a year later, nobody remembers the previous interaction.

Rye models the entire pipeline as a graph with full history.


1. Entity Mapping

Real-world thingnode_typeKey properties
Candidatepersonfirst_name, last_name, email, linkedin_url
Open roleopportunityname, code, department, level, location
Interviewevent_type: interviewround, format, duration
Referral sourceperson or orgStandard properties
Relationshipedge_typeDirection
Candidate applied to roleapplied_toperson -> opportunity
Interviewer assessed candidateinterviewedperson -> person
Employee referred candidatereferralperson -> person
Role belongs to teamassigned_toopportunity -> org
Hiring manager owns roleassigned_toopportunity -> person

2. Pipeline as a CRM Opportunity

Recruiting reuses the CRM pipeline model. An open role is an opportunity with its own pipeline stages:

INSERT INTO nodes (node_type, label, properties) VALUES (
    'pipeline', 'Recruiting Pipeline',
    '{
        "name": "Recruiting Pipeline",
        "code": "PL-RECRUIT",
        "stages": [
            {"key": "applied", "label": "Applied", "order": 1},
            {"key": "phone_screen", "label": "Phone Screen", "order": 2},
            {"key": "technical", "label": "Technical Interview", "order": 3},
            {"key": "onsite", "label": "Onsite / Final", "order": 4},
            {"key": "offer", "label": "Offer Extended", "order": 5},
            {"key": "hired", "label": "Hired", "order": 6, "terminal": true},
            {"key": "rejected", "label": "Rejected", "order": 7, "terminal": true},
            {"key": "withdrawn", "label": "Withdrawn", "order": 8, "terminal": true}
        ],
        "default_stage": "applied"
    }'
);

Creating a role and a candidate

-- Open role
SELECT create_opportunity(
    'Senior Backend Engineer',
    'PL-RECRUIT',
    (SELECT id FROM nodes WHERE label = 'Jane Smith' AND node_type = 'person'),
    '{"department": "Engineering", "level": "senior", "location": "remote", "salary_range": "150k-180k"}',
    ARRAY['engineering', 'recruiting']
);

-- Candidate
INSERT INTO nodes (node_type, label, properties, attrs) VALUES (
    'person', 'Alex Rivera',
    '{"first_name": "Alex", "last_name": "Rivera", "email": "alex@example.com", "source": "referral"}',
    '{"teams": ["recruiting"], "classification": "internal"}'
);

-- Link candidate to role
INSERT INTO edges (edge_type, source_id, target_id, properties)
SELECT 'applied_to', candidate.id, role.id,
    '{"applied_date": "2024-03-15", "source": "referral", "resume_ref": "s3://resumes/alex-rivera.pdf"}'
FROM nodes candidate, nodes role
WHERE candidate.label = 'Alex Rivera'
  AND role.properties->>'code' LIKE 'OPP-%'
  AND role.label = 'Senior Backend Engineer';

3. Interview Tracking

Each interview is an event with structured feedback as an assertion:

-- Log a phone screen
SELECT record_event(
    p_event_type     := 'interview',
    p_summary        := 'Phone screen with Alex Rivera for Senior Backend Engineer',
    p_properties     := '{"round": "phone_screen", "format": "video", "duration_minutes": 45}',
    p_participant_ids := ARRAY[
        (SELECT id FROM nodes WHERE label = 'Alex Rivera'),
        (SELECT id FROM nodes WHERE label = 'Jane Smith'),
        (SELECT id FROM nodes WHERE label = 'Senior Backend Engineer' AND node_type = 'opportunity')
    ]::uuid[],
    p_participant_roles := ARRAY['candidate', 'interviewer', 'regarding'],
    p_actor          := 'user:jane',
    p_occurred_at    := '2024-03-20T14:00:00Z'::timestamptz
);

-- Record interviewer feedback as an assertion (use the event ID returned above)
INSERT INTO assertions (assertion_type, subject_node_id, claim, confidence)
VALUES (
    'interview_feedback',
    (SELECT id FROM nodes WHERE label = 'Alex Rivera'),
    '{
        "round": "phone_screen",
        "decision": "advance",
        "strengths": ["distributed systems experience", "clear communication"],
        "concerns": ["limited Go experience"],
        "notes": "Strong candidate. Recommend technical round.",
        "interviewer": "Jane Smith"
    }',
    0.85
);

4. Advancing Through the Pipeline

-- Move candidate's application from phone_screen to technical
-- (Uses the same CRM pipeline stage mechanism)
SELECT advance_deal_stage(
    (SELECT id FROM nodes WHERE label = 'Senior Backend Engineer' AND node_type = 'opportunity'),
    'technical',
    'Alex Rivera passed phone screen, scheduling technical',
    'user:jane'
);

For per-candidate stage tracking (multiple candidates per role), use a candidate_stage assertion on the candidate node instead of the role node:

INSERT INTO assertions (assertion_type, subject_node_id, claim, confidence)
VALUES (
    'candidate_stage',
    (SELECT id FROM nodes WHERE label = 'Alex Rivera'),
    '{"stage": "technical", "role_code": "OPP-2403-0042", "moved_from": "phone_screen"}',
    1.0
);

5. Key Queries

Pipeline funnel for a role

SELECT
    ca.claim->>'stage' AS stage,
    count(*) AS candidates
FROM edges app
JOIN nodes candidate ON candidate.id = app.source_id AND candidate.node_type = 'person'
JOIN current_assertions ca ON ca.subject_node_id = candidate.id AND ca.assertion_type = 'candidate_stage'
WHERE app.target_id = (SELECT id FROM nodes WHERE label = 'Senior Backend Engineer' AND node_type = 'opportunity')
  AND app.edge_type = 'applied_to' AND app.archived_at IS NULL
  AND ca.claim->>'role_code' = 'OPP-2403-0042'
GROUP BY ca.claim->>'stage'
ORDER BY min(
    CASE ca.claim->>'stage'
        WHEN 'applied' THEN 1 WHEN 'phone_screen' THEN 2
        WHEN 'technical' THEN 3 WHEN 'onsite' THEN 4
        WHEN 'offer' THEN 5 WHEN 'hired' THEN 6 ELSE 7
    END
);

Candidate’s full history (for reapplication)

-- All events involving this candidate
SELECT
    e.occurred_at, e.event_type, e.summary, ep.role, e.properties
FROM events e
JOIN event_participants ep ON ep.event_id = e.id
WHERE ep.node_id = (SELECT id FROM nodes WHERE label = 'Alex Rivera')
ORDER BY e.occurred_at DESC;

-- All assertions (current and superseded)
SELECT assertion_type, claim, asserted_at, superseded_at
FROM assertions
WHERE subject_node_id = (SELECT id FROM nodes WHERE label = 'Alex Rivera')
ORDER BY asserted_at DESC;

Time-to-hire metrics

SELECT
    role.label AS role_name,
    candidate.label AS candidate_name,
    app.properties->>'applied_date' AS applied,
    hire_a.asserted_at AS hired_date,
    hire_a.asserted_at::date - (app.properties->>'applied_date')::date AS days_to_hire
FROM edges app
JOIN nodes candidate ON candidate.id = app.source_id
JOIN nodes role ON role.id = app.target_id
JOIN current_assertions hire_a ON hire_a.subject_node_id = candidate.id
    AND hire_a.assertion_type = 'candidate_stage'
    AND hire_a.claim->>'stage' = 'hired'
WHERE app.edge_type = 'applied_to' AND app.archived_at IS NULL;

6. Agent Interaction Examples

Agent: “Where are we on the senior backend engineer search?”

Queries the role’s pipeline funnel, lists candidates by stage, surfaces any blocked interviews or overdue feedback.

Agent: “Alex Rivera just applied again. What happened last time?”

Retrieves Alex’s full assertion and event history, including previous interview feedback, the stage they reached, and why they didn’t advance (e.g., a candidate_stage assertion with stage = 'rejected').

Agent: “Schedule a technical interview for Alex with two backend engineers.”

Uses record_event() to create the interview event with Alex and the two engineers as participants, then creates a task (“Conduct technical interview for Alex Rivera”) assigned to the interviewers.