companyId argument; if omitted, the call is scoped to the user’s primary company. See Multi-company access for when to pass it.
Discovery
listCompanies
List the Alex companies this token has access to. The LLM should call this first whenever a user has multiple companies, or whenever it needs to translate a company name into a companyId.
Inputs
query(string, optional) — substring to filter company names. Only used by Alex platform admins (who can list across all companies).
Search
searchCandidates
Natural-language candidate search across the talent pool. Backed by the same search infrastructure as Talent Match in app.alex.com.
Inputs
query(string, required) — natural-language description: role, skills, location, experience, education, prior employers, work preferences.limit(number, default 10) — max results.radius(number, optional) — location radius in miles when a location is in the query.recency(number, optional) — only return candidates updated within this many days.status(string[], optional) +statusOperator("includes" | "excludes") — filter by candidate status.companyId(string, optional).
searchDocumentation
Searches Alex’s product documentation and returns a synthesized answer with citation links. Useful when users ask “how do I…?” rather than asking about their data.
Inputs
query(string, required) — the user’s question, in natural language.
Jobs and pipeline
getJobDetails
Fetch a single job’s configuration plus current pipeline counts and recent sessions.
Inputs (any one of)
jobId(string) — internal Alex position UUID.externalId(string) — ATS job ID.jobName(string) — fuzzy match on role title.sessionLimit(number, default 10) — recent sessions to include.sessionStatus(string, optional) — filter sessions by status (e.g."COMPLETED").companyId(string, optional).
getJobsSummary
Summary of all jobs (or one user’s jobs) with completion rates, average scores, and decision breakdowns.
Inputs
userId(string, optional) — scope to one user’s created jobs.userName(string, optional) — scope by recruiter name (resolved server-side).sinceDate/untilDate(ISO date strings, optional) — filter by job creation date.includeClosedJobs(boolean, default true).status(string, optional) — filter by job status (e.g."ACTIVE","PAUSED").limit(number, default 50, max 200) — max jobs returned per call.offset(number, default 0) — pagination offset; combine withlimitto page through large companies.companyId(string, optional).
totalJobs, openJobs, closedJobs, activeJobs, pausedJobs, etc.) are always over the full filtered set regardless of limit/offset. The jobs array is the page, and hasMore indicates whether more pages exist.
Each job in the jobs array now carries both isOpen (open vs closed for applications) and status ("ACTIVE", "PAUSED", etc.).
ATS coverage — output also includes an atsCoverage block aggregated from ats_job_coverage_weekly and filtered by the same date window:
supported—trueonly when the company’s ATS has weekly coverage rows. Bullhorn-only today; non-Bullhorn customers getsupported: falseand null fields.atsType— the ATS type for the rolled-up rows.atsJobsCreated— total active jobs created in the customer’s ATS during the window.alexJobsImported— how many of those landed in Alex.alexJobsWithInterviews— Alex jobs that actually ran at least one interview.coveragePercent—alexJobsImported / atsJobsCreated.interviewPercent—alexJobsWithInterviews / atsJobsCreated.weeksCovered— number of weekly rows the rollup spans.
- “Give me a summary of Sarah’s jobs from this quarter.”
- “Show me the next 50 active jobs.” (paginates with
limit: 50,status: "ACTIVE") - “How many of Acme’s ATS jobs are running through Alex this year?”
getCandidatePipelineStats
Candidate counts grouped by interview status, decision, or as a funnel view.
Inputs
metric("byStatus" | "byDecision" | "funnel", required).positionId(string, optional) — scope to one job.sinceDate/untilDate(optional).companyId(string, optional).
getPositionStats
Position-level metrics: open positions count, total positions, candidates per position.
Inputs
metric("openPositions" | "totalPositions" | "candidatesPerPosition", required).sinceDate/untilDate(optional).status(string, optional) — filter by position status (e.g."ACTIVE").companyId(string, optional).
Interviews
getInterviewGuideDetails
The interview guide (questions, criteria, intro/outro, learning objectives) for a position. Useful for asking “how should I improve my questions?” or “what’s in my guide?”
Inputs — none required (uses the current job context if available, otherwise pass companyId and the LLM may need a position context first).
Example prompt: “Are there gaps in our interview questions for backend engineers?”
getInterviewStats
Interview-volume metrics: completed interviews, completed candidates, average duration, breakdowns by interviewer, by status, or by medium type (video vs phone vs SMS), top candidates.
Inputs
metric("completedCandidates" | "completedInterviews" | "averageDuration" | "byInterviewer" | "byStatus" | "byMediumType" | "topCandidates", required).positionId(string, optional) — auto-scopes to current job context if available.companyWide(boolean, optional) — explicitly query across all jobs.sinceDate/untilDate(optional).limit(number, optional, max 100) — fortopCandidates.decision(("NONE" | "ACCEPTED" | "REJECTED" | "SUBMITTED" | "PLACED" | "HIRED")[], optional) — fortopCandidates.mediumType(("VIDEO" | "PHONE" | "EXTERNAL" | "BOTH" | "WHATSAPP" | "PHONE_SMS" | "SMS" | "NONE")[], optional) — filter any metric by interview medium. For a full breakdown, usemetric: "byMediumType"instead.companyId(string, optional).
- “What’s the top 10 highest-scoring candidates for the Atlanta Sales Lead role?”
- “How many phone vs video interviews did we run last quarter?” (
metric: "byMediumType") - “How many phone interviews has AlexAI completed this month?” (
metric: "completedInterviews",mediumType: ["PHONE"])
getInterviewTimeOfDayBreakdown
Hour-of-day distribution of completed interviews plus a headline after-hours coverage % — answers “what share of interviews happen outside business hours?”.
Inputs
sinceDate/untilDate(ISO date, optional) — defaults to the last 30 days.timezone(IANA name, optional, defaultAmerica/New_York) — buckets are computed in this zone; Postgres handles DST automatically.businessHoursStart/businessHoursEnd(0–23 / 1–24, optional, defaults9and18, end-exclusive).positionId(string, optional) — auto-scopes to current job context if available.companyWide(boolean, optional).companyId(string, optional).
inHoursCount / afterHoursCount / afterHoursPercent.
Example prompt: “What share of Acme’s interviews happened after 6pm last quarter?”
getCandidateReport
Look up a candidate’s interview report by name, email, or position. Returns one or more candidate records with deep links into Alex, plus per-session ratings, reviews, decision, integrity flags, and invite-to-completion timing.
Inputs (one of name or email required)
name(string, optional) — partial match, order-agnostic ("liu chao"matches"Chao Liu").email(string, optional) — exact match.jobName(string, optional) — narrow to a specific job title.limit(number, default 5) — max sessions per candidate.status(string, optional) — filter sessions by status.companyId(string, optional).
decision—Decisionenum (NONE | ACCEPTED | REJECTED | SUBMITTED | PLACED | HIRED).recruiterRating,alexRating—Session.feedbackScoreandSession.alex_feedback_score(numeric, nullable).recruiterReview,alexReview—Session.feedbackandSession.alex_feedbacktext (nullable).notes— up to 10 most-recent recruiter notes attached to the report (report_note.content, ordered desc).inviteToCompletionSeconds— time between invite (Session.dateCreated) and completion (Session.time_completed);nullif the session isn’t completed.fraudHighRisk—truewhen the candidate’soverall_verification_scoreis below 50 (mirrorsgetFraudVerificationStats);nullwhenfraud_detection_enabledis off.fraudVerificationScore— rawoverall_verification_score(0–100);nullwhen fraud detection is off.cheatingFlagged—truewhenbase_evaluation.cheating_score >= Company.cheat_threshold;nullwhengemini_cheat_detection_enabledis off.cheatingScore— rawcheating_score;nullwhen cheating detection is off.
- “Show me Anthony’s interview reports.”
- “Pull last quarter’s hires for Acme — include their ratings and reviews so I can highlight a few in the renewal recap.”
Integrity (cheating, fraud, identity)
Three distinct concepts in Alex, each with their own tool. Don’t confuse them:
For a single candidate,
getCandidateVerification returns all three at once.
getCheatingDetectionStats
Cheating-detection metrics — flagged candidates, flagged interviews, percentages, per-job rates, and individual flagged sessions with full sub-score breakdown (eye/head tracking, voice analysis, transcript analysis, multi-person detection).
Inputs
metric("flaggedCandidates" | "flaggedInterviews" | "flaggedPercentage" | "byJob" | "flaggedDetails", required).positionId(string, optional).companyWide(boolean, optional).sortByJobMetric("rate" | "count", optional, forbyJob).sinceDate/untilDate(optional).companyId(string, optional).
fraud_detection_enabled or gemini_cheat_detection_enabled on the company.
Example prompt: “Which jobs have the highest cheating rate this month?”
getFraudVerificationStats
Identity-signal metrics from SEON — disposable emails, VOIP phones, VPN/proxy IPs, thin LinkedIn, digital-footprint anomalies. Aggregates over candidate_verification.
Inputs
metric("flaggedCandidates" | "riskBreakdown" | "flaggedDetails", required).threshold(number, optional, default 50) —overall_verification_scorebelow this counts as flagged.sinceDate/untilDate(optional).limit(number, optional, max 50) — forflaggedDetails. Default 20.companyId(string, optional).
fraud_detection_enabled on the company.
Example prompt: “How many candidates this month had VPN-flagged IPs or disposable emails?”
getIdentityVerificationStats
Stripe document + selfie verification metrics. Aggregates over identity_verification.
Inputs
metric("statusBreakdown" | "verificationRate" | "failureReasons" | "failedDetails", required).sinceDate/untilDate(optional).limit(number, optional, max 100) — forfailedDetails. Default 20.companyId(string, optional).
enable_identity_verification on the relevant jobs.
Example prompt: “What’s our ID-verification completion rate this month, and why are candidates failing?”
getCandidateVerification
The full integrity picture for ONE candidate (or session): cheating sub-scores, fraud signals, identity-verification status. The right tool for “is this candidate legit?” or “why was X flagged?”
Inputs (any one of)
sessionId(string) — most precise.candidateId(string) — returns the most recent session’s verification.email(string) — exact match.name(string) — partial / order-agnostic.companyId(string, optional).
base_evaluation row exists; identity block is null if Stripe ID verification wasn’t run for that job).
Example prompt: “Show me everything we know about jane@acme.com’s identity.”
Account
getCompanyFeatures
Which Alex features are enabled for the current company (fraud detection, resume fit, talent match, coordinator, WhatsApp, custom logo, etc.). Useful for the LLM to know whether to suggest a feature.
Inputs
featureName(string, optional) — check one specific feature; otherwise returns all.companyId(string, optional).
getAtsConnection
Inspect how the company’s ATS integration is configured: which ATS is set, whether credentials are populated (field-presence only — values are never returned), Kombo aggregator status, the auto job-import flag, and whether decision-stage sync mappings are configured.
Use this for “is this customer connected to X?”, “what ATS does Y use?”, “why isn’t sync working?”, “is auto job import on?”.
Inputs
companyId(string, optional).
atsType+atsLabel(e.g."BULLHORN"/"Bullhorn").connected(boolean) — at least one expected credential field for the configured ATS is populated.expectedCredentialFields/configuredCredentialFields/missingCredentialFields— credential field names only.kombo—{ integrationId, status, configured }for the Kombo aggregator (used for Avature, SAP SuccessFactors, SmartRecruiters, and as an alt path for some other ATSes).autoJobImportEnabled(boolean).syncFieldsConfigured(boolean) — at least one decision-stage sync field+value pair (hired/placed/rejected/shortlisted/submitted/no-decision) is set.
listTemplates
Lists templates the company has defined and reports adoption per type — ”% of active jobs using a template”. Covers two template surfaces: resume-fit (drives resume scoring) and interview-guide (structured guides). Each entry carries an adoptionMethod field describing how the number was computed, and the two differ in how much they can be trusted:
resumeFitis exact. The template id survives on the job (Interviewer.resume_fit_template_id), so both the adoption rate and the per-template job counts are real.interviewGuideis a proxy. A guide template copies its content into the job and drops the template id, leaving only asource='template'mark on the copied questions and grading criteria — so adoption is counted as “the job’s guide has at least one template-marked question or grading criterion”. The bucket-level rate is meaningful; per-templatejobCountis always 0 and is not usage. The mark is also copied when a guide is duplicated, so it can be present on a job that nobody created from a template. Treat it as company-level adoption, not as a per-job fact.
This previously reported two separate interview-guide numbers — one for grading criteria, one for question text. They have been merged into a single
interviewGuide figure: the split implied a precision the underlying data doesn’t have, the two barely overlapped, and each half alone undercut real adoption. The merged number reads higher than the old criteria-only one; that is the same population counted correctly, not growth.templateType("resumeFit" | "interviewGuide", optional) — drill into a single type; otherwise returns both.topN(number, optional, default 10, max 50) — top templates per type to include.includeUnused(boolean, optional, default false) — only meaningful forresumeFit;interviewGuidealways lists templates alphabetically since per-template usage is unattributable.companyId(string, optional).
totalTemplates, jobsUsingType, adoptionPercent, adoptionMethod, and a topTemplates array. Active job = Interviewer.status='ACTIVE' AND is_deleted=false (matches getJobsSummary and getPositionStats).
Example prompts
- “What % of Acme’s active jobs use a resume-fit template?”
- “Which interview-guide templates does Acme have set up?”
Calling a tool directly
If you’re building a non-LLM client and want to call a tool over JSON-RPC:result.content[0].text as a human-readable string. (Internally each tool returns structured data; the server formats it for LLM and human consumption.)