MCP connector¶
The Zero Hiring MCP connector exposes 13 domain-scoped tools that forward to the local zero CLI.
Agents should prefer these tools over shelling out to zero directly.
Each tool takes the same payload shape:
command(string, required) — Zero action handled by this tool, e.g.jobs listor--help.args(string[], optional) — positional values, flags, and flag values. Do not put the action inargsonly.
To discover flags for any action, call get_help with the action and --help:
tool: get_help
command: "candidates list"
args: ["--help"]
For workflows, pricing, and Jenny-specific guidance, see the agent skill reference.
Tools¶
Getting help¶
| Tool ID | get_help |
| Access | Read-only |
Inspect the current Zero account, discover available Zero actions, and answer product-knowledge questions from the real Help Center. For 'what account / employer / user am I?' use command: "whoami" — it returns email, company, and employer_id from the backend. For 'what commands exist?' use command: "--help" (or command: "<subcommand>", args: ["--help"]). Also handles status show and billing reads. For a PRODUCT-KNOWLEDGE question -- 'how does sourcing work', 'what can you do', 'what happens when a candidate applies', 'why did a candidate get that score' -- use command: "help articles search", args: [<the question, quoted or as separate words>] FIRST, before doing anything else. Do NOT answer this shape of question by reading your own skill file over bash, running --help on unrelated commands, or inspecting account/candidate data to infer an answer -- that flailing is the specific failure this search exists to replace: in September production telemetry, 'how does the zero hiring sourcing work?' cost ~21 model round trips and 'what can you do for me? like a list if possible?' cost ~13, against ~3 for a well-sourced data question. Each search result carries a url -- the canonical Help Center link (/dashboard/help/<category>/<slug>) -- cite it in your answer so the operator can click through, and quote/summarize only from help articles show <category> <slug>'s returned content, never from memory or invented feature names. If search returns nothing relevant, say plainly that the Help Center doesn't cover it rather than guessing. This reads the same live corpus the in-app Help Center serves, so a just-edited article shows up immediately with no separate sync step.
Also accepts root discovery calls such as --help, --version, and <action> --help.
Allowed commands
help, whoami, status show, billing docs, billing show, billing costs, billing usage, billing sourcing-usage, billing invoices
Managing authentication¶
| Tool ID | manage_auth |
| Access | Write |
Log in, log out, or update local authentication settings.
Allowed commands
login, logout, internal configure
Reading jobs¶
| Tool ID | read_jobs |
| Access | Read-only |
List jobs and inspect role details. For RPO accounts, use clients list first when the operator names a client, then pass --client-id <client_uuid> to jobs list or internal jobs list to see jobs under that specific client. When checking whether a role or requisition exists, or when enumerating a customer's roles, never conclude a job does not exist from a single unpaginated page. Prefer jobs list: it already returns every job for the employer in one call, so it needs no pagination flag. --all exists ONLY on internal jobs list (which otherwise returns just the first 20 rows) -- pass it in the same call rather than listing once and re-listing, and never pass --all to jobs list, which rejects it. If a result still carries a truncation warning (JSON: a jobs_list_truncated entry; table: a Showing N of T or use --all footer line), say so rather than presenting the list as complete. When a question is scoped to specific locations (city/state/site), answer from the location fields of the full paginated list, or pass the location as --search; never infer a job's location from its title or from earlier conversation context. Job list rows do NOT contain description text -- they carry description_chars and description_omitted instead, because a few dozen full descriptions exceed this tool's result limit. description_omitted: true means the job HAS a description that was left out; it never means the job lacks one (that is description_chars: 0). To answer 'which roles mention X?', pass internal jobs list --search X, which matches description text server-side. To read one description, use jobs show <job>. Only pass --with-description when you genuinely need every description at once, and expect a very large result. Use jobs followup-rules list <job> / jobs followup-rules preview <job> <rule_id> to read scheduled pipeline follow-up emails (chase-the-AI-interview, request-availability style nudges to candidates already in the pipeline) and who a rule would email right now; creating/pausing/resuming/deleting one is on manage_jobs below. Use jobs timeline <job> for "what happened on this job, in order" -- a single reverse-chronological feed merging job creation, stage changes, notes, interviews, scores, feedback, ATS sync, AI decisions, and email outreach across every candidate on the role. Pass --since <date> (an ISO date or datetime) to scope it to "this week" or a similar recent window instead of paging through the full history. Use account timeline on read_admin_data instead when the question spans every job on the account rather than one role. Use jobs talent-pool list <job> (ZER-3215/ZER-3884) for people who applied to a DIFFERENT role and were scored against this one -- already reachable about it under a recorded lawful basis (reuse_basis on each row), so check this before starting a new sourcing search. --view all also shows rows already dismissed on this job (jobs talent-pool dismiss, on manage_jobs). For "summarize the requisition for role X" -- or any question needing the JD, screening questions, hiring-flow (stage) config, AND pipeline counts for one role -- use jobs requisition <job> (ZER-3448) in a SINGLE call. Do NOT assemble this by calling jobs show (JD only) plus hiring-flow show (stage config only) plus applications stage-counts (counts only): that three-call minimum is exactly what jobs requisition replaces, and it is also the first read path this CLI has ever had for screening questions after they were saved -- jobs show/internal jobs get do NOT return them (only internal jobs create/internal jobs update ever echoed them back, and only in that same request/response). screening_questions: null in the result means none were ever saved; an empty array means they were saved and then explicitly cleared -- state which one plainly rather than treating both as "no questions".
Allowed commands
jobs list, jobs current, jobs requisition, jobs show, jobs timeline, jobs followup-rules list, jobs followup-rules preview, jobs talent-pool list, internal jobs list, internal jobs get, internal jobs external-postings list
Managing jobs¶
| Tool ID | manage_jobs |
| Access | Write |
Create, update, delete, select, or parse roles. hire run --description and internal jobs create/update --description are source material for the hosted JD generator; use --use-description-as-jd only for an explicit polished HTML final JD; plain text is still formatted with AI. Once you've run intake and assembled a structured role summary, pass it as --role-summary-json (a JSON object with at least title) on hire run: it is threaded into both JD generation and the job record so the JD, the job, and the pipeline brief all match the confirmed intake. To take a filled or paused role down, use jobs close <job> --yes (optionally --reason "filled"): the role stops being listed and stops accepting applications, outreach on it stops, and every application and interview stays in the operator's lists. jobs delete is different: it archives the role out of the lists entirely. A role that still owes its close fee is refused with a 409 that says so; explain that plainly and do not retry. To bring a closed role back, use jobs reopen <job> --yes (ZER-3305): it makes the role listed and open to applications again and is harmless on an already-active job. A role that was never paid for (its checkout was never completed) is refused with a 409 saying so -- reopening does not itself charge anything, so explain the refusal plainly and do not retry; a role that WAS paid before it closed reopens with no new charge. For RPO accounts, pass --client-id <client_uuid> on hire run or internal jobs create to create the role under that client; omit it only for roles owned directly by the agency. For requests to create, add, set up, update, or load screening questions, interview questions, or custom questions, generate with internal ai generate-questions --structured --json when needed, then save with internal jobs update <job_id> --screening-questions-json <json>; the JSON must be an array of {text,order,source} question objects; source is optional but if set must be one of manual/ai/jenny/ats/legacy (any other value errors before the request is sent — ZER-1154 — instead of silently saving nothing) and the command errors loudly, after re-fetching, if fewer questions persisted than were sent. --custom-questions is legacy fallback only. For Google for Jobs (structured location + salary; ZER-1083), hire run, internal jobs create, and internal jobs update all accept: a remote flag to mark a fully remote role — --remote/--no-remote on hire run (also feeds JD generation), --is-remote/--no-is-remote on internal jobs create/internal jobs update; --location-street, --location-locality, --location-region, --location-postal-code, --location-country (2-letter ISO 3166-1 country code, e.g. US) for the physical address; and --salary-min, --salary-max, --salary-currency (3-letter ISO 4217, e.g. USD), --salary-period (one of YEAR, MONTH, WEEK, DAY, HOUR) for the pay range. All four salary flags are MANDATORY when creating a role with hire run or internal jobs create (ZER-2420): the CLI refuses the call with missing_salary, naming every flag you left out, and the backend rejects it with a 422 -- so settle the range during intake (internal ai suggest-salary computes one) instead of opening the role without it. They stay optional on internal jobs update, which only changes what you pass. --salary-visibility public|private (ZER-4072, on hire run, internal jobs create, and internal jobs update) is the "show the range to candidates" switch: public (the default) shows it; private keeps it off the public posting and other candidate-facing surfaces while still pricing the open fee and showing on employer views. Setting private is refused with a 422 naming the law wherever a pay-transparency rule requires the range in the posting, or the role's location can't be pinned down -- surface that 422 to the operator verbatim rather than retrying. internal jobs get echoes the stored value back as salary_visibility, plus salary_visibility_locked and salary_visibility_note (the law, when locked). hire run also accepts --budgeted-compensation <annual USD, whole dollars> (ZER-3277): the basis for the role-open fee (3% of this x positions; 6% total at hire, less what was already paid). It defaults to --salary-max when --salary-period is YEAR and --salary-currency is USD; otherwise pass it explicitly or the command errors under --yes naming the flag. Not needed with --skip-payment/--demo/--dry-run. The five location flags are optional, but the backend enforces them conditionally and returns a 422 with a clear message on violation: for a non-remote role, once ANY of the five location flags is set, --location-country plus --location-locality or --location-region become required (street/postal stay optional); --is-remote skips this entirely. On internal jobs update, once --salary-min or --salary-max is set, --salary-currency and --salary-period both become required. Surface a 422 or a missing_salary error on these flags to the operator verbatim rather than retrying blindly; it names the missing field. The existing freeform --location still works and keeps being used for display/back-compat. For a recruiter touch to automate -- "chase candidates who haven't started the AI interview" or "ask candidates in resume screening 3+ days for their availability" -- use jobs followup-rules create <job> --trigger stage_age --stage resume_assessment --min-days 3 --subject '...' --body '...' (or --trigger ai_interview_not_started, no --stage needed). This only schedules future sends: eligibility (unsubscribed/rejected/hired, already replied) is rechecked right before each one, and nothing goes out until a mailbox is connected -- check sending_identity_ready on jobs followup-rules preview (read_jobs) before telling the operator a rule is live. --schedule weekly --weekday <0-6> --time HH:MM --timezone <IANA> repeats every week; the default --schedule once emails each matching candidate a single time. jobs followup-rules pause|resume|delete <job> <rule_id> manage an existing rule; find rule ids with jobs followup-rules list on read_jobs. jobs talent-pool dismiss <job> <match_id> (ZER-3703/ZER-3884) marks a talent-pool match "not a fit for this role" -- it only hides it from this job's default jobs talent-pool list; talent-pool undismiss reverses it. jobs invite-candidates <job> --applicant <id> [--applicant <id> ...] --yes (ZER-2732) adds existing candidates to this job's normal pipeline; if the job has a default outreach sequence each newly-added candidate with a usable email is enrolled and may get the first email right away -- the same real-world effect as outreach enroll, so it needs the same explicit human --yes. jobs approve-description <job> --yes (ZER-1866/ZER-2489) records that a named human approves the job's CURRENT description text -- call it only immediately after a real human has actually read that exact text and said yes; it attributes the approval to a named person on the audit record exactly like a hire/offer confirmation, and never accepts new description text itself.
Allowed commands
hire run, jobs use, jobs close, jobs reopen, jobs delete, jobs followup-rules create, jobs followup-rules pause, jobs followup-rules resume, jobs followup-rules delete, jobs talent-pool dismiss, jobs talent-pool undismiss, jobs invite-candidates, jobs approve-description, internal jobs create, internal jobs update, internal jobs delete, internal jobs parse-jd, internal jobs external-postings create, internal jobs external-postings update, internal jobs external-postings delete, internal jobs set-scorecard-template
Reading candidates¶
| Tool ID | read_candidates |
| Access | Read-only |
Review candidates, applications, interview results, and transcripts. Use command: "candidates search" with the free-text query as the first arg (e.g. args: ["senior react engineer"], optionally with --source applied|sourced|ats, --page, --limit) when the operator asks something like 'who in my pool has done X', 'do we have any candidates who know Y', or otherwise wants to search ALL of the employer's candidates across every job and integration by name, email, title, skills, or resume content, including semantic matching -- it is not scoped to one job and takes no job id. Use candidates list <job> instead for job-scoped pipelines (rankings, per-job status, resume review for one role's applicants). Do NOT use candidates list once per role to work out a pipeline total -- those reads are paginated and summing them across roles produces a different answer each time (ZER-2590); call pipeline summary on read_pipelines instead, which returns the authoritative cross-role total in one call. For "who is stuck at resume screening", "who is awaiting a decision", "who is in AI interview", or any question naming a pipeline stage/status across roles, use command: "internal applications list" with --stage-type <type> (applied| resume_assessment|ai_interview|human_interview|hired|rejected), optionally --stage-status entered|completed and/or --status pending|hired|rejected|transferred, and leave --job-id UNSET -- do NOT answer this by listing every job with jobs list and then paging candidates list <job> (a full, unpaginated per-job dump) once per role to filter by stage yourself. That per-role fan-out is the single worst tool loop measured in Jenny's September production traffic: roughly 200 model requests and ~$3.50 of tokens for ONE question ("can you give me a list of candidates stuck at resume screening"), 25-30x costlier than any other shape asked that month. internal applications list with --job-id omitted is already tenant-scoped and paginated across every role in ONE call; its printed/--json total is the response's own X-Total-Count for these exact filters -- never re-derive a cross-role total by paging per role and counting rows, and never trust a page as complete without checking that total. --stage-status REQUIRES --stage-type (rejected with an error otherwise -- a bare status matches every stage type at once). status='pending' (across every role) IS the definition of "awaiting a decision" -- do not re-derive it from stage buckets. For EXACT criteria rather than fuzzy intent, use command: "candidates count" — it answers 'how many people do we have who ...' with the count as the actual result. Flags compose with AND: --min-years, --max-years, --title, --company, --location-country (2-letter code), --location-region, --location-contains, --skill (repeatable, or comma-separated) with --skills-mode all|any, --source applied|sourced|ats, --applied-within-days N (last applied within the last N days), --applied-before YYYY-MM-DD (last applied strictly before this date; combine both for a window). Never work a total out by paging candidates filter (or candidates list) and counting rows -- call candidates count. Use candidates filter with the same flags (plus --page/--limit) once the operator wants to see the actual people; its total is the same number candidates count returns. 'Who applied this week / in the last N days', 'how many are still fresh', and 'who has gone cold' questions are answered with candidates count using --applied-within-days / --applied-before -- never by paging and counting. MANDATORY when reporting either: read back the response's headline sentence, not just the number. Some filterable fields (years of experience, location, current title) are unknown for part of the population, and headline states how many candidates were excluded ONLY because a field is unknown for them rather than because they fail the filter. Presenting a bare count as if it were complete is the specific failure this endpoint exists to prevent. Keep using candidates search for vague or semantic questions -- these filters are exact-match only. To deliver a FILE of candidates (a CSV someone asked for), use candidates export-csv with the same flags as candidates filter — it is generated server-side from the query and returns a permalink to link. NEVER build a file by hand: do not page candidates filter/candidates list and re-type/paste the rows you read into a CSV or spreadsheet yourself -- that costs tokens on every row and risks a transcription mistake the server-generated file cannot make, because it comes straight from the same filter query candidates count/candidates filter run. Link the returned permalink, never download_url (a short-lived direct link that expires). Its row_count always equals what candidates count reports for the same flags. Use candidates export <applicant_id> (singular, no relation to export-csv above) to produce a GDPR subject-access / portability bundle for ONE candidate (Art. 15/20); pass the applicant_id from candidates detail --json. The bundle is sensitive PII — return only what the requester needs and do not retain it. Use candidates notes list <applicant_id> for prior context before or during a candidate interview -- freeform notes a recruiter (or you) already left on this person, newest first. Use candidates timeline <applicant_id> instead for "what happened with this person, in order" -- a single reverse-chronological feed merging stage changes, notes, interviews, scores, feedback, ATS sync, AI decisions, and email outreach. Reach for notes list when the need is specifically the human-written context; reach for timeline for the full history. This candidates timeline is the person grain only -- for "what happened on this job" or "what happened on this account", use jobs timeline <job_id> (on read_jobs) or account timeline (on read_admin_data) instead; neither the job nor account roll-up is missing, they just live on those tools. Both take the applicant_id from candidates detail --json, not a candidate number.
Allowed commands
candidates list, candidates detail, candidates search, candidates count, candidates filter, candidates export-csv, candidates export, candidates notes list, candidates timeline, results show, results detail, offers show, offers template show, offers template preview, offers template variables, internal applications list, internal applications stage-counts, internal applications assessment-rationale, internal applications decisions, internal applications same-title-interview, internal interviews results, references status, ats phone-review list
Managing candidates¶
| Tool ID | manage_candidates |
| Access | Write |
Invite, send offers, reject, discard sourced candidates, rank, create applications or interviews, and upload files. Use candidates discard when the user wants to remove a sourced candidate from active sourcing; it also stops future outreach (the web UI's equivalent control is labeled "Stop outreach" -- same action). Use candidates hire to send a SignWell offer; billing happens after candidate signature (6% of the hire's guaranteed cash compensation, less the role-open fee already paid; ZER-3277). Pass --guaranteed-cash-compensation <annual USD, whole dollars> when the operator states the actual offer compensation -- it's captured on the offer for that calculation; omitted, it falls back to the role's budgeted compensation. close run remains a legacy alias. The backend refuses candidates hire/close run with an HTTP 409 hire_requires_candidate_contact when there is no evidence this candidate has ever actually engaged with us (no direct application, no reply to outreach, no interview they engaged with) -- this is a deliberate refusal, not a transient error; explain to the operator that the candidate needs recorded contact evidence first, do not retry the same call, and do not suggest a flag to bypass it (there is none). Use invite cancel <interview_id> to withdraw a pending interview invite before the candidate has started it (rejected with a conflict once started/finished). Use offers cancel <offer_id> to withdraw an in-flight offer letter; it voids the SignWell document and emails the candidate -- held by the confirm gate (needs --yes after an explicit operator yes, same as sending one). The step-by-step offer loop: offers create <application_id> drafts the letter (sends nothing; optional --guaranteed-cash-compensation <annual USD>), offers update <offer_id> --compensation ... --start-date ... edits the draft, offers show <offer_id> reviews it (read_candidates), then offers send <offer_id> --yes emails the candidate the e-signature offer -- only after the operator explicitly approves in this turn; never invent --yes. The server still refuses an incomplete offer (it names the missing fields: report them and fix with offers update or company settings) and a second in-flight offer on the role (explain it; --force only if the operator explicitly wants competing offers). The company letter template is edited with offers template update (--body-file, {{variable}} placeholders; read offers template variables and offers template preview first in read_candidates), and offers template import <file> converts an existing PDF/Word letter into a template body without saving it unless --save. Hire/offer approval stays with the human: drafting is yours, sending is theirs to confirm. Use candidates delete <applicant_id> --confirm to permanently erase ALL data for a candidate (GDPR Art. 17 right to erasure) — this is irreversible, requires the explicit --confirm flag, and should only be run to fulfil a verified erasure request. internal applicants create makes a brand-new, standalone applicant with no link to the sourcing pool -- use it only for a genuinely new candidate (a referral or direct apply) who was never sourced. Never call internal applicants create for someone who already has a sourced profile_id (from sourcing search or candidates list --source sourced): that creates a disconnected duplicate that breaks provenance and dedupe against the sourcing pool. Promote that person instead with sourcing add <profile_id> through manage_outreach (ZER-1650: this exact mistake required a hand repair of prod data on 2026-07-20). For invite send, always pass --interview-type explicitly. Use realtime for audio, phone_call for phone call / AI phone call / AI telephony invites, and video for video/avatar interview invites. Example phone args: ["--candidate", candidate, "--interview-type", "phone_call"]; example video/avatar args: ["--candidate", candidate, "--interview-type", "video"] (the CLI defaults video invites to the Jenny Anam avatar, avatar_id 80d32804-14cc-445c-9005-e641b41f8f18, unless an explicit --avatar-id is provided). Ambiguous invite send calls without --interview-type are rejected. For employer preview links after creating a role, use internal interviews test --job-id <job_id> --interview-type video --json; it creates a test interview with the same default Jenny Anam avatar and returns a clickable interview_url. For reference checks, prefer the automatic candidate-led flow: references request <application_id> emails the CANDIDATE (never a referee) a link to list their referees and consent; each referee is then invited automatically and reminded, and references status <application_id> (read_candidates) shows N of M responded and an AI summary that informs the operator and never decides. It is refused with HTTP 409 reference_request_requires_candidate_contact when the candidate never engaged with us -- explain plainly, do not retry, there is no override. Use the manual path only when the operator already has a referee's details: first internal interviews create-reference --application-id <id> --job-id <job_id> --name <name> --email <email> to record who the reference is (returns a reference_id), then internal interviews create --interview-type video --applicant-id <id> --job-id <job_id> --use-case reference --reference-id <reference_id> to send the reference a link to a video interview with Jenny (reference checks are video only; phone_call is rejected). create-reference alone does not contact anyone. The same candidate-contact-evidence gate as candidates hire applies to the reference-check create call: HTTP 409 reference_request_requires_candidate_contact means this candidate has no recorded engagement with us yet -- explain that plainly, do not retry, there is no override. Use candidates notes add <applicant_id> --body "..." to leave a freeform note on a candidate -- the same kind of prior-context note a recruiter would jot down before or during a candidate interview. There is no author flag: the note is attributed to your credential automatically. Use candidates text <n> --body "..." --yes to send one text message to a candidate who agreed to text messages when they applied (refused if they did not or replied STOP; deferred outside 08:00-21:00 their local time); it cannot be unsent, so pass --yes only after explicit operator confirmation. Use candidates notes edit <note_id> --body "..." --applicant <applicant_id> to fix a note's wording (server-side author-only: 403 if you didn't write it), and candidates notes delete <note_id> --applicant <applicant_id> --yes to permanently remove one added to the wrong candidate or that should never have been written down -- irreversible, no undo, requires the same explicit --yes after operator confirmation as candidates reject. --applicant is required on both: the backend addresses a note by applicant + note id together, and candidates notes list --json returns each note's applicant_id alongside its note_id. Use integrations advance --stage-id <id> (--ats-application-id <id> | --external-id <id>) --yes to move an application's stage in the connected ATS, integrations reject (--ats-application-id <id> | --external-id <id>) --yes to attempt a rejection there (always 422s today -- today's connected-ATS integration layer has no rejection endpoint for any provider -- but still needs --yes: Zero never rejects a candidate automatically on an AI assessment alone), integrations note (--ats-candidate-id <id> | --external-id <id>) --body "..." --yes to attach a note to the candidate's connected-ATS record (NOT the same as candidates notes add, which writes to Zero's own notes), integrations write-back-report (--ats-application-id <id> | --external-id <id>) --yes to attach the interview score-report PDF there, and integrations loxo write-back-interview <interview_id> --yes for the Loxo-specific analog. All five are held by the confirm gate (ats_writeback). candidates reject takes an optional --rejected-by recruiter|candidate|hiring_manager (ZER-4002) naming who actually made the call -- the candidate withdrew/declined, or the client's hiring manager said no, as opposed to your own screening decision. Pass it only when the operator says so; omitted, the platform records recruiter on its own. Use candidates unreject to undo a rejection -- it only works on a candidate whose current status is rejected, restores whichever status they had immediately before the reject (usually pending), clears the stored rejection reason, and never emails the candidate. Before undoing "the last rejection" on a role, confirm which candidate that actually was with jobs timeline <job_ref> (read_jobs) -- the merged, reverse-chronological feed for the whole role -- rather than guessing from list order. Use internal applications transfer <application_id> --target-job <job_ref> --reason "..." (ZER-4035) to move a candidate to a different role of the same employer; a reason is required. The source application becomes transferred and a new pending application is created on the target role (its AI interview, if any, stays on the original application); nothing is emailed to the candidate or the employer. A 409 (same_job/target_job_closed/already_on_target_job/source_not_movable/status_changed) means the move was refused -- explain which from the error message rather than retrying blind.
Allowed commands
invite send, invite manual, invite cancel, candidates hire, candidates reject, candidates unreject, candidates discard, candidates delete, candidates notes add, candidates notes edit, candidates notes delete, candidates text, offers cancel, offers create, offers update, offers send, offers template update, offers template import, close run, close charge-fee, internal applications create, internal applications rank, internal applications score-resume, internal applications decisions-review, internal applications human-review-request, internal applications record-contact, internal applications reprocess, internal applications automation-exemption, internal applications transfer, internal interviews create, internal interviews create-reference, references request, internal interviews test, internal interviews trigger-call, internal interviews transcript-translate, internal applicants create, internal applicants update-phone, internal applicants update-email, ats phone-review update-phone, ats phone-review resume-interview, integrations advance, integrations reject, integrations note, integrations write-back-report, integrations loxo write-back-interview, internal files upload, internal files publish-url
Reading outreach¶
| Tool ID | read_outreach |
| Access | Read-only |
Review saved sourcing searches, shortlists, sequences, enrollments, and outreach stats. Does not run sourcing search; use manage_outreach with command: "sourcing search" for that. Use command: "sourcing status" with args [search_job_id] to check a search that is still running — a search reported as sourcing_search_timeout was NOT cancelled, and this is how you pick it back up. Poll it every ~15s for at most 10 checks; once status is succeeded it returns a search_id, and sourcing show <search_id> returns the ranked results. If it is still running after 10 checks, hand the user the search_job_id and stop polling rather than looping on it. Never respond to a timed-out search by re-running it through manage_outreach: that bills a second provider call for a search already in flight. outreach sequences results <sequence_id> and outreach jobs results <job_ref> report enrolled/replied/interviewing counts (per-sequence or rolled up across every sequence on a role); outreach jobs preview-variables <job_ref> shows exactly how a sequence's {{job_title}}/{{role_summary}}/{{company_name}} placeholders will render for that role. outreach company-sequence show reads the employer's one company-wide default sequence. outreach linkedin-tasks list lists LinkedIn manual-step tasks waiting on a recruiter to send by hand. outreach talent-pool list lists candidates reachable about a role they did not apply to. outreach send-status and outreach throughput report today's sending cap and this month's remaining contact capacity.
Allowed commands
outreach sequences example-steps, outreach sequences list, outreach sequences show, outreach sequences results, outreach enrollments list, outreach stats, outreach jobs results, outreach jobs preview-variables, outreach company-sequence show, outreach linkedin-tasks list, outreach talent-pool list, outreach send-status, outreach throughput, sourcing show, sourcing status, sourcing shortlist list
Managing outreach¶
| Tool ID | manage_outreach |
| Access | Write |
Run sourcing searches and manage shortlists, outreach sequences, and enrollments. For a normal sourcing search, call this tool once with command: "sourcing search" and args [query, "--job", job_ref, "--limit", limit, "--depth", "cheap", "--wait", "--wait-timeout", "90", "--json"], where job_ref is either the full job UUID or the 8-char short id exactly as shown by jobs list — never construct, pad, or guess a UUID; pass the short id as-is and the CLI resolves it. Always pass --wait-timeout 90: this tool is killed at 120s, and without the cap a slow search lost its search_job_id with the process instead of handing it back (ZER-1687); a --wait search whose --wait-timeout is missing or larger than that ceiling is rejected before any provider call is billed. Use the user's requested count when it is below 50, otherwise use "50" when the user asks for 50+, or gives no count. Run at most one sourcing search per user sourcing request; after one successful result, summarize and ask whether to continue rather than self-initiating another paid search. If the search returns code: "sourcing_search_timeout", the search is STILL RUNNING and was not cancelled — the message carries its search_job_id. Tell the user it is still running, then poll it with read_outreach command: "sourcing status" args [search_job_id] every ~15s and read the results with sourcing show <search_id> once it succeeds. Poll at most 10 times; if it is still running after that, hand the user the search_job_id and stop rather than polling indefinitely. Never re-run the search in that situation: the provider call is already billed and in flight, and a second one double-charges for the same work. For every explicit user request to run a normal sourcing search, you must invoke this tool with command: "sourcing search" — never answer from conversation memory or prior results. The one exception is the timeout case above, where the correct action is to poll the existing search rather than start a new one. This does not apply to explicit fake/demo, safe test-candidate, or experimental paper-mining requests, which use their documented commands below. If sourcing returns code: "sourcing_location_ambiguous" in blocks or warnings, do not retry or broaden the search; ask the user to clarify the location with country or City, Region/State, Country. Use "--source-more" only when the user asks to continue or get more of the exact same search without changing any wording of the criteria; if the new request's wording differs from the previous query in any way — added, removed, or reworded titles, locations, or qualifiers — run a fresh search using the user's exact wording as the query and do not pass "--source-more". Do not add keywords, industries, seniority, or other qualifiers the user did not say. If the user says to use an exact query or ignore job requirements, include "--query-only"; the job is still used for ranking/attribution but provider filters come from the query. If the user explicitly names multiple countries, cities, or regions, do not split them into separate tool calls; append one repeatable --location value per requested location in the same args array so the backend performs one combined search and the limit applies once across all locations. Use --limit 1 only for explicit smoke tests. Read the returned diagnostics before explaining outcomes: distinguish provider_empty from profiles returned but deduped_existing_count > 0, and never claim the provider database has no profiles solely because result_count is 0. If diagnostics include zero_results_reason: "filter_exhausted", tell the user this exact search was already exhausted on the filter_exhausted_since date and that rewording the search or asking again after the backend's TTL will re-check the network; do not report it as "no candidates exist". In dev/staging, normal sourcing returns demo candidates; if the user explicitly asks for real candidates, add --real and the backend caps that live non-production call to 5 profiles. To promote a sourced candidate into the job's applicants list, call command: "sourcing add" with args [profile_id, "--job", job_ref], using the profile_id from a sourcing result -- this is the only correct path from a sourced profile to an applicant and it keeps provenance/dedupe intact against the sourcing pool. Never use internal applicants create (a manage_candidates command) for a candidate who already has a sourced profile_id; that creates a disconnected duplicate instead of promoting the existing sourced record. For fake/demo sourcing, call command: "sourcing demo-candidates add" with args ["--job", job_id, "--json"]; this creates the same five fake sourced candidates, including Muluken and Alan, without calling PDL. For safe demos, create a sourced-style test candidate with command: "sourcing test-candidate add" and args ["--job", job_id, "--email", test_email, "--phone", test_phone, "--json"]; this uses the real sourced application/outreach pipeline without emailing real sourced candidates. For stop-only requests like 'stop outreach for Jane but keep them in sourcing', use command: "outreach enrollments stop-by-candidate" with args [candidate_ref, "--job", job_id, "--json"]; this stops future emails without discarding the candidate. For outreach sequences create in MCP/Jenny, pass steps with --steps-json; the server cannot read Claude-local files or stdin. Use outreach cancel <enrollment_id> (optionally with --reason and --burn-bridges) as the stronger revert primitive: it terminates an active/paused enrollment like stop does, and --burn-bridges additionally blocks that candidate from ever being re-enrolled into the same sequence/job again. For an experimental paper-mining search, call command: "sourcing experimental search" with args ["--job", full_job_uuid, "--limit", "10", "--wait", "--wait-timeout", "90", "--json"] (same 120s ceiling and same sourcing status follow-up as a normal search); it mines recent academic papers for researchers in the role's field, ranks them by citation velocity, and adds them to the job. This is experimental: best for research-heavy roles, and contact-data coverage varies. outreach sequences update <sequence_id> edits an existing sequence's name, sender identity, or steps (same --steps-json requirement as create); pass only the fields to change. outreach jobs default-sequence ensure <job_ref> attaches the role's default sequence if it has none yet; the first time it actually attaches one, it also enrolls every already-sourced candidate on that role and starts emailing them immediately, so it requires --yes and is held for operator confirmation exactly like outreach enroll. outreach jobs default-sequence reset <job_ref> only rewrites that sequence's step wording/cadence and is never held. outreach company-sequence reset regenerates the employer's one company-wide sequence from the current template; it affects every role still linked to it. outreach linkedin-tasks complete <task_id> records that you already sent a LinkedIn manual-step message yourself and resumes the sequence; call it only after you have actually sent that message.
Allowed commands
sourcing search, sourcing experimental search, sourcing demo-candidates add, sourcing add, sourcing shortlist add, sourcing test-candidate add, sourcing invite, sourcing estimate, account mailboxes test-send, outreach sequences create, outreach sequences update, outreach sequences activate, outreach sequences deactivate, outreach enroll, outreach cancel, outreach enrollments pause, outreach enrollments resume, outreach enrollments stop, outreach enrollments stop-by-candidate, outreach jobs default-sequence ensure, outreach jobs default-sequence reset, outreach company-sequence reset, outreach linkedin-tasks complete
Reading pipelines¶
| Tool ID | read_pipelines |
| Access | Read-only |
Inspect hiring pipeline runs and platform status. pipeline summary is the single source of truth for "how many candidates are in our pipeline" questions: one aggregate call returning per-role stage counts and the authoritative cross-role total. Always use it instead of jobs list plus one candidates list per role -- that hand-rolled roll-up returned three different totals for the same unchanged data (ZER-2590). Report its total_candidates as-is; do not re-add the per-role numbers, and never fall back to scripting/paging over candidates list to build your own total -- if a question is scoped by role or client, narrow THIS call instead of listing more broadly and filtering yourself. Add --include-closed for closed roles too, --client-id <uuid> to scope to one RPO client, and --search <text> to match a role's title/description/location -- e.g. "the Ashley client's sales manager roles" is --client-id <Ashley's uuid> --search "sales manager", still one call. Each role also reports awaiting_decision (applications nobody has hired/rejected yet). For "which of my roles has gone quiet", "what's the state of my roles", or any question needing per-role LAST-ACTIVITY alongside pipeline counts, use pipeline role-activity instead -- do NOT answer it with jobs list followed by one jobs timeline/candidates list per role to work it out by hand. That per-role fan-out is exactly what cost 30-173 seconds and 11-39 tool calls for one question, and the resulting hand-rolled cross-role total came out different on every ask -- 550, 610, 610, ~620, and 700 candidates for the SAME unchanged data (ZER-2590; measured again in the ZER-3441 latency benchmark). pipeline role-activity is the one call that replaces the whole loop: each role's last_activity_at (the same event feed as its account-wide timeline), total_candidates, interviewed_count, and stage breakdown, plus the authoritative cross-role totals, in a single aggregate read. It reports raw timestamps rather than a pre-baked "quiet" flag -- compare last_activity_at against the response's own generated_at yourself to answer "how many days is quiet"; do not guess a cutoff, and do not re-derive it by listing jobs and then checking each one's timeline. If this command reports not_supported (the backend hasn't deployed it yet), fall back to pipeline summary for candidate/stage counts and say plainly that last-activity data isn't available yet -- do not fall back to the per-role jobs timeline loop this command exists to replace. pipeline reports list / pipeline reports show <id> read the account's scheduled recurring pipeline reports (ZER-3732); creating, pausing, or canceling one is on manage_pipelines. pipeline pending-approvals (ZER-3884) lists runs waiting on a human right now, each naming its pending_approval phase -- approve or reject it with pipeline advance on manage_pipelines.
Allowed commands
status show, pipeline status, pipeline list, pipeline summary, pipeline role-activity, pipeline reports list, pipeline reports show, pipeline pending-approvals
Managing pipelines¶
| Tool ID | manage_pipelines |
| Access | Write |
Create, advance, or abort hiring pipeline work. Demo-building and demo pipeline simulation moved to manage_demo (ZER-3104) -- demo build, demo run, and demo populate are not reachable here; use manage_demo for all of those. Schedule a recurring pipeline report (ZER-3732) when a user asks for one on a cadence ("send me this pipeline report every Monday"): pipeline reports create --channel <dashboard_chat|email|slack> [--job <job>] [--cadence weekly|daily] [--day monday] [--time 08:00] [--timezone <IANA name>]. Omit --job for the whole account; deliver to the channel the user asked from (dashboard_chat in the dashboard chat, slack in the team's chat workspace) unless they name another, and ask for their timezone rather than assuming UTC. pipeline reports update|pause|resume|cancel <id> change, pause, restart, or delete one; find ids with pipeline reports list on read_pipelines. ATS controls: integrations sync resyncs from the connected ATS; integrations writeback set --mode auto|off --auto-invite|--no-auto-invite [--invited-stage <name>] [--completed-stage <name>] changes write-back; integrations job-selection set and integrations loxo scope choose what imports. Switching --mode auto (writes stages into the customer's own ATS) or --auto-invite (emails new applicants unprompted) ON needs --yes after an explicit operator yes; both are held for confirmation (--mode auto under ats_writeback, --auto-invite under start_outreach). Turning them off needs none. integrations disconnect [--client-id <id>], integrations loxo disconnect --agency-id <id> and integrations loxo webhooks unregister --agency-id <id> remove part of the customer's own ATS connection (reconnecting needs the customer's sign-in or API key again), so each needs --yes after an explicit operator yes and is held for confirmation (ats_writeback); without --yes they refuse with confirmation_required. pipeline record-approval <run_id> --phase <phase> --decision approved|rejected (ZER-3884) is a second door onto the SAME floor pipeline advance --approve/--reject guards -- --phase must match the run's CURRENT phase. An 'approved' decision on awaiting_sequence_approval or awaiting_hire_approval is a human floor (ZER-3748) even on Autopilot and needs the same explicit human --yes as pipeline advance; a 'rejected' decision never does. pipeline set-slack-thread <run_id> --thread-ts <ts> (ZER-662) records which Slack thread a run's future updates should post into.
Allowed commands
pipeline reports create, pipeline reports update, pipeline reports pause, pipeline reports resume, pipeline reports cancel, integrations sync, integrations writeback set, integrations job-selection set, integrations loxo scope, integrations disconnect, integrations loxo connect, integrations loxo disconnect, integrations loxo webhooks register, integrations loxo webhooks unregister, pipeline run, pipeline advance, pipeline abort, pipeline autopilot, pipeline record-approval, pipeline set-slack-thread
Reading admin data¶
| Tool ID | read_admin_data |
| Access | Read-only |
Review team members, employer analytics, and RPO clients. Use integrations status / integrations connections for whether the customer's ATS is connected and its sync state, and integrations writeback show for the ATS write-back and auto-invite settings; integrations connect only explains how a person connects one (it needs their own sign-in; never attempt it). Use clients list to see all managed clients; clients show <client_id> for details. In dev/staging only, internal jenny calls|chats ... reads Jenny live-call transcripts and chat logs for debugging (returns 404 in production). Use account timeline for "what happened on this account, in order" -- a single reverse-chronological feed merging job creation, stage changes, notes, interviews, scores, feedback, ATS sync, AI decisions, email outreach, and team member changes across every job on the account. Pass --since <date> (an ISO date or datetime) to scope it to "this week" or a similar recent window instead of paging through the full history. Use jobs timeline <job> on read_jobs instead when the question is scoped to one role. clients sender-domain show <client_id> (ZER-901/ZER-3884) reads a client's own candidate-email sending domain and whether it's verified. clients parse-company-info, clients parse-context-answers, clients preview-company-answer, clients analyze-company-context, and clients fetch-logo all write nothing -- they only check, transcribe, preview, or look up; save any result yourself with clients update/clients confirm-company-profile. reviewers list / reviewers invitations list (ZER-3884) read the client-scoped Hiring Managers invited to review candidates -- distinct from team, which has no client scoping. audit-log list (ZER-4117) reads the employer's customer-visible audit log -- security, candidate-data access, and hiring-timeline events, newest first, filterable by --category security|access|hiring, --actor-user-id, --since/--until, and paginated with a keyset --cursor (pass the previous call's next_cursor to continue). audit-log export returns the same filters as a CSV file. Admin and account manager only -- the backend 403s any other role; explain that plainly rather than retrying. account reporting ai-overrides (ZER-4128) reports how often reviewers' decisions differed from the AI recommendation -- override rate per job, per reviewer and per pipeline stage, each with the sample count it was taken from, plus a flag on a reviewer who almost never overrides over a meaningful sample. Rates and counts only, never a raw AI score. Pass --job-id to scope to one role.
Allowed commands
team list, team invitations list, team join-requests list, team join-setting show, team billing-activity, team my-employers, internal employers analytics, clients list, clients show, clients sender-domain show, clients parse-company-info, clients parse-context-answers, clients preview-company-answer, clients analyze-company-context, clients fetch-logo, reviewers list, reviewers invitations list, integrations status, integrations connections, integrations connect, integrations writeback show, integrations job-selection show, integrations job-selection catalog, integrations loxo companies, integrations loxo sub-agencies, integrations loxo webhooks list, account rejection-reasons show, account rejection-reasons stats, account sender-domain show, account reporting show, account reporting ai-overrides, account export, account calendars list, account mailboxes list, account mailboxes routing-impact, internal jenny calls list, internal jenny calls show, internal jenny chats list, internal jenny chats show, account timeline, audit-log list, audit-log export
Managing admin data¶
| Tool ID | manage_admin |
| Access | Write |
Invite teammates by email with team invite (team add is a legacy invitation alias), remove members, change employer records, manage RPO clients, or run internal AI helpers. This tool is write-access, so most MCP clients require a human to approve each call before it runs. If a call here (including internal employers create) returns something like "No approval received", that is the MCP client declining to run an unapproved write call -- it is not a Zero backend error, not a permissions or allowlist failure, and not a regression in org creation. Tell the operator to approve the tool call in their client and retry. This is unrelated to the real backend spend-approval gate on payments refund (see the Refunds section of the skill), which requires a live human typing approved_by at an interactive prompt and is genuinely unsatisfiable over MCP by design -- unlike this per-call client approval, that command has no MCP tool at all, on purpose. team invite --email <email> --employer-id <id> (ZER-1185) targets an employer other than your own -- this only works for an allowlisted demo-admin inviting into an employer flagged is_demo=true (e.g. adding a presenter/teammate to a demo org you just created with internal employers create --demo); anyone else passing a non-own --employer-id gets a 403. Omit it to invite into your own employer as before. For RPO client management: clients add --name <name> creates a new client; clients remove <client_id> soft-deletes one; clients update <client_id> partial-updates one (pass only the fields to change). clients refresh-company-profile (ZER-3676/3677) re-reads the caller's OWN website into the saved company profile, merging onto it -- a hand-edited field always survives. clients complete-onboarding marks the caller's own employer as done with onboarding (idempotent). clients mine-company-questions (ZER-2762) scans recent interview transcripts for candidate questions and folds them into the FAQ inbox -- safe to re-run any number of times. clients sender-domain set <client_id> --email <address> (ZER-901) provisions that client's own sending domain for candidate email; sender-domain verify <client_id> checks the published DNS records. Neither sends anything to a candidate by itself. For reviewers (client-scoped Hiring Managers, ZER-3884): reviewers invitations send --email <email> --client <client_id> invites one scoped to that client; reviewers invitations revoke <id> cancels a pending invite; reviewers remove <hiring_manager_id> revokes an accepted reviewer's job access without touching feedback they already left. internal ai generate-jd accepts --role-summary-json (a JSON object with at least title) to fold the confirmed structured intake into the generated JD, and --markdown to convert the draft from HTML to Markdown -- use --markdown whenever the draft is going into a chat reply (ZER-2468), since the chat renderer refuses raw HTML; never paste the plain --json job_description field into a reply. Use internal ai generate-questions --structured --json to draft role-specific screening, interview, or custom questions, then save them with manage_jobs internal jobs update --screening-questions-json; do not stop at a draft unless the operator explicitly asked to review one first. internal ai suggest-salary --title <title> --location <location> (ZER-2467) computes a recommended salary range during intake -- salary is never optional and never something to skip asking about. Present the result as an ESTIMATE the operator owns, never as market data Zero measured (there is no benchmark data source behind it, it's an LLM estimate), and offer exactly three options: accept the suggested range, give a different number, or (only when the response's disclosure_required is false) not disclose one. When disclosure_required is true, a range is legally required for that posting -- say so instead of silently dropping salary or treating non-disclosure as available. internal employers create provisions a demo tenant with --demo (plus optional --logo-url/--website-url) -- but for a NEW demo org you almost always want manage_demo's demo build instead: it does org creation, role(s), pipeline population, and the login mint as ONE call. Use internal employers create directly only when you want a bare org with no roles/pipeline/login yet. The login mint itself (demo login create) and the demo-simulation commands (demo build, demo run, demo populate) all live on manage_demo, not here. team invite stays the way to add named internal people (presenters, sales) to a demo org -- it is not the customer handoff. demo mode on|off (flips demo mode on an existing employer without deleting any data) moved to manage_demo (ZER-3104 follow-up) -- not reachable here.
Allowed commands
team invite, team add, team remove, team member-add, team set-role, team set-billing-access, team invitations resend, team invitations revoke, team join-requests approve, team join-requests decline, team join-setting set, clients add, clients remove, clients update, clients refresh-company-profile, clients complete-onboarding, clients mine-company-questions, clients sender-domain set, clients sender-domain verify, account sender-domain set, account sender-domain verify, account rejection-reasons set, reviewers invitations send, reviewers invitations revoke, reviewers remove, internal employers create, internal ai generate-jd, internal ai generate-questions, internal ai extract-skills, internal ai suggest-salary, internal ai draft-job-from-intake, internal ai plan-job-changes
Building a demo¶
| Tool ID | manage_demo |
| Access | Write |
Build a demo org and simulate its hiring pipeline. demo build (ZER-3104) is the single-call way to build a whole demo: it creates the demo org, opens one job per --role with an AI-generated JD, populates each with a realistic pipeline, and mints the customer-facing login -- all in ONE write call, e.g. command: "demo build", args: ["--company", "Acme Robotics", "--website", "acmerobotics.com", "--role", "Senior Backend Engineer", "--role", "Product Designer", "--json"]. Pass --website whenever the operator gives one, so the backend resolves a real logo server-side (Logo.dev re-host, favicon fallback) -- never construct or guess a logo URL yourself (a 2026-09-01 incident hand-crafted one against a third-party logo API that had already been shut down months prior). --employer-id resumes an existing demo org instead of creating a new one -- use it to add more roles to an org demo build already created, or to finish a build that failed partway through; the response's resume_command field (or a role/login marked "status": "failed") names the exact follow-up call. This tool is write-access, so most MCP clients require a human to approve the call before it runs -- but demo build needs exactly ONE such approval for an entire demo build, versus the ~7 separate approvals the old multi-call sequence (internal employers create --demo + hire run --demo per role + demo populate per role + demo login create) needed; tell the operator they can grant "Always allow" narrowly to manage_demo specifically, without opening up any other write tool, if their client offers per-tool trust. If a call here returns something like "No approval received", that is the MCP client declining to run an unapproved write call -- it is not a Zero backend error, not a permissions or allowlist failure, and not a regression in demo building. Tell the operator to approve the tool call in their client and stay on the tab until it returns, then retry; do not conclude the build failed or that org creation is broken. demo run (single hardcoded role, no org creation) and demo populate <job_id> (pipeline-only, on an already-open job) stay available for the narrower cases demo build doesn't cover -- demo populate <job_id> --reset (ZER-3153) re-rolls a job whose roster came out wrong (e.g. it fell back to an irrelevant fixture draw) by purging its seeded pipeline and cached roster before populating fresh, instead of rebuilding the whole demo org; demo login create --employer-id <id> mints/rotates the login on its own, e.g. after --skip-login or a login-only resume. demo mode on|off --employer-id <id> flips demo mode on an EXISTING employer (one not created via demo build) without deleting any data -- do NOT call demo mode on after a demo build: a build already provisions the org with is_demo: true (same as internal employers create --demo), so demo mode on against it is a no-op that only costs an extra approval.
Allowed commands
demo build, demo run, demo populate, demo login create, demo mode
Reading the changelog¶
| Tool ID | read_changelog |
| Access | Read-only |
See what's new on the platform: recent releases merged across the whole product (backend + client), most recent first. Use command: "changelog show" for a 'what's new' / 'what shipped recently' / 'what are your new features' question -- optional --since <ISO date> and --limit <N> (default 10) narrow the window. This always reads the live feed at call time; never answer a changelog question from memory or from your own tool list -- an answer whose items don't trace back to this feed is the failure mode this tool exists to prevent. Do not filter results on a release's highlight field: it only controls whether the in-app What's New modal auto-opens, not whether the release shipped, and most releases have it set to false.
Allowed commands
changelog show
Reading interviews¶
| Tool ID | read_interviews |
| Access | Read-only |
List and inspect human (hiring-manager) interviews -- the scheduling coordination flow between a candidate's application and an interviewer, distinct from AI screening interviews. interviews list filters with --job-id, --application-id, --status (one of pending_slots, awaiting_candidate, scheduled, completed, cancelled, declined, expired), and --upcoming (only scheduled interviews with a future start time, soonest first; otherwise newest first). Example: command: "interviews list", args: ["--job-id", job_id, "--status", "awaiting_candidate", "--json"]. interviews show <interview_id> returns the full interview including its slots array (each with start_at, end_at, status), scheduling_mode (proposed_slots or booking_link), booking_window_end (booking_link only), plus applicant_name, job_title, interviewer_display_name, and (employer-side) schedule_url -- the public link the candidate uses to pick a time; share the candidate's scheduling status rather than the raw link unless the operator explicitly asks for the URL. interviews availability (--interviewer-user-id or --interviewer-email, plus optional --duration, --days, --from, --timezone) checks the interviewer's connected Google Calendar for free slots -- always call this before proposing times so slots come from real availability; the response's connected field is false when no calendar is linked, in which case propose times manually instead. Pass --from YYYY-MM-DD to search a specific window (e.g. "next week") instead of always getting this week's slots -- it is interpreted in the --timezone zone and --days counts forward from it; omit it to start from today. Pass --window-days (10, 15, or 20) or --window-start/--window-end instead to preview a booking link's window -- every open time in the window, summarized as a total/day count plus a per-day breakdown, the same preview interviews create --booking-link is about to send the candidate. interviews hours show returns the interviewer's own weekly hours, booking rules (rules: minimum notice, buffer, start-time interval, daily limit, candidate change cut-off), date overrides (with override_id) and public-holiday country; interviews hours holiday-countries lists valid codes. Cancelled interviews in show/list --json carry cancelled_by (candidate or recruiter) and cancel_reason -- a candidate's own reason when they self-cancelled. interviews feedback-list <application_id> (ZER-3401) returns the application's scorecards (recommendation, per-attribute scores, summary), legacy_ratings (old 1-5 ratings) and hidden_count (scorecards withheld until the caller submits their own); interviews scorecard-templates lists the scorecard templates and their attributes.
Allowed commands
interviews list, interviews show, interviews availability, interviews hours show, interviews hours holiday-countries, interviews needs-reply, interviews completed, interviews feedback-list, interviews legacy-feedback, interviews scorecard-templates, interviews accommodations
Managing interviews¶
| Tool ID | manage_interviews |
| Access | Write |
Create, propose slots for, reschedule, cancel, complete, or leave feedback on a human (hiring-manager) interview. interviews create requires --application-id plus an interviewer: either --interviewer-user-id <uuid> (a platform member/reviewer) or --interviewer-email (optionally with --interviewer-name) for an ad-hoc interviewer. Optional: --title, --duration (minutes, 5-480, default 45), --location-type (one of video, phone, onsite; default video), --location-details (meeting URL / phone note / address), --timezone (IANA, e.g. America/New_York; default: this machine's timezone, else UTC -- ZER-3774), --message (shown to the candidate). --booking-link (ZER-3774) sends one link so the candidate books any open time on the interviewer's calendar instead of picking from --slot times -- mutually exclusive with --slot. Its window defaults to 10 work days; override with --window-days (10, 15, or 20) or a custom --window-start/--window-end (YYYY-MM-DD, mutually exclusive with --window-days). The backend 409s no_calendar/needs_reauth when the host has no usable calendar connection -- the CLI's error message already suggests falling back to --slot; relay it as-is. Only one active (non-terminal) human interview is allowed per application at a time; the CLI surfaces the backend's conflict error if one already exists. Once that interview reaches a terminal status (completed, cancelled, declined, expired), a further interviews create call succeeds and starts the next round -- the backend auto-increments a round number per application; interviews show/interviews list --json return it, and the job's human_interview stage config may separately carry an advisory expected_rounds -- surface both when relevant rather than presenting a second interview as unexpected. Status machine: pending_slots (created, no slots yet) -> awaiting_candidate (slots proposed, candidate emailed) -> scheduled (candidate picked a time; confirmations + calendar invites sent) -> completed | cancelled | declined (candidate said no times work) | expired (all proposed slots passed unpicked). declined/expired can be re-proposed back to awaiting_candidate with interviews propose-slots. Propose 1-5 --slot values (repeatable) on interviews create or interviews propose-slots <interview_id>; each must be ISO8601 with an explicit UTC offset or Z, e.g. 2026-08-01T15:00:00-04:00 -- times without a timezone are rejected. Creating with --slot proposes immediately and emails the candidate that same call; creating without --slot leaves the interview in pending_slots for a later interviews propose-slots call. Example: command: "interviews create", args: ["--application-id", application_id, "--interviewer-email", "hm@client.com", "--interviewer-name", "Jamie Lee", "--slot", "2026-08-01T15:00:00-04:00", "--slot", "2026-08-02T15:00:00-04:00", "--json"]. interviews reschedule <interview_id> takes new --slot values (1-5, same ISO8601 rule) and an optional --reason; it clears any existing selection and re-emails the candidate, moving the interview back to awaiting_candidate. --booking-link (ZER-3799) reschedules to a new booking link instead -- same --window-days/--window-start/--window-end, --duration, --location-type, --location-details, --message, and --timezone (default: this machine's timezone) options as interviews create --booking-link, and mutually exclusive with --slot; those booking-link options are rejected without --booking-link. interviews cancel <interview_id> takes an optional --reason and works from any non-terminal status; notifies the candidate (and the interviewer too, if it had already been scheduled). Confirm with the operator before cancelling an interview that is already scheduled -- the candidate and interviewer have a specific time on their calendar. interviews complete <interview_id> marks a scheduled interview done. interviews scorecard <interview_id> (ZER-3401) submits the structured scorecard: --recommendation (strong_no|no|yes|strong_yes), one --score "<attribute name or position>=<1-5>" per attribute of the job's template (list them with read_interviews interviews scorecard-templates; omit --score when the job has no template), optional --note "<attribute>=<text>" and --summary. Re-submitting updates your existing scorecard. interviews feedback <interview_id> (--rating 1-5 and/or --note) is DEPRECATED -- use interviews scorecard. interviews nudge <interview_id> re-sends the candidate's existing scheduling link -- use it for "nudge them" / "remind them to pick a time" requests instead of interviews propose-slots or interviews reschedule with the same slots, which would re-propose the offer rather than just re-emailing it. Only valid while the interview is awaiting_candidate; the backend returns HTTP 409 with a plain-English reason otherwise (e.g. already scheduled) -- relay that message rather than retrying. Not confirmation-gated: it sends one email the operator explicitly asked for. interviews no-show <interview_id> (optional --note) marks a scheduled interview whose start has passed as a no-show; terminal, no candidate email, no undo; 409 invalid_state/not_started otherwise. Availability settings: interviews hours set --day mon=09:00-17:00 --day sat=off --timezone <IANA> --holiday-country <CC|none> (only named days change), interviews hours override-add <YYYY-MM-DD> [--hours HH:MM-HH:MM|off] [--note], interviews hours override-remove <override_id>; add --user-id to manage a colleague (owner/member only). Booking rules: interviews hours set-rules [--min-notice 1h|2h|4h|8h|12h|24h|48h] [--buffer 0|5|10|15|30|45|60] [--step 15|30|60] [--daily-cap 1-10|none] [--change-cutoff 2|4|12|24|48|none] changes only the rules passed (minutes of buffer/step; cut-off in hours before the interview, none = candidates can always reschedule or cancel); it applies to every booking link for that interviewer and does not touch weekly hours. Before proposing slots, call read_interviews interviews availability for the interviewer and propose 2-5 of the returned times -- when the interviewer has Google Calendar connected, scheduling also creates a real calendar invite with an automatic Google Meet link (no need to set --location-details for video interviews in that case).
Allowed commands
interviews create, interviews propose-slots, interviews reschedule, interviews cancel, interviews complete, interviews feedback, interviews scorecard, interviews nudge, interviews no-show, interviews hours set, interviews hours set-rules, interviews hours override-add, interviews hours override-remove
Reading the hiring flow¶
| Tool ID | read_hiring_flow |
| Access | Read-only |
Show a job's hiring flow (stage plan) -- the resume gate, AI interview, and optional reference/human interview checkpoints a candidate moves through -- or draft a new one with AI. hiring-flow show <job_id> returns the saved plan, or the fully-automated smart default (resume gate -> AI interview) if none has been saved yet. hiring-flow draft is stateless -- it does not save anything -- and takes optional --role-summary-json (same shape as hire run --role-summary-json), --job-title, and --feedback to steer a redraft. Call hiring-flow draft right after role intake, before opening the role, so the operator confirms the flow alongside the JD; there are only three stage types -- resume_assessment, ai_interview, human_interview -- and human_interview is present in every draft but disabled by default unless the role warrants it -- say so plainly rather than letting the operator assume every draft includes a live human step. A reference check is NOT a stage type: it is a sibling reference_check config on the plan (enabled, count 1-5), not a fourth stage -- building a reference_interview stage object is rejected by the backend.
Allowed commands
hiring-flow show, hiring-flow draft
Managing the hiring flow¶
| Tool ID | manage_hiring_flow |
| Access | Write |
Save a hiring flow (stage plan) for a job with hiring-flow set <job_id> --stages-json <json>. Accepts either a bare JSON array of stage objects, or a full {"stages": [...], "source": "...", "reference_check": {...}} object -- pass an edited hiring-flow draft output straight through. --source defaults to manual and should stay that way once the operator has reviewed or edited the draft; only use ai to persist an unedited AI draft as-is. Enabling the resume_assessment stage syncs its config.threshold into the job's real auto-invite gate; disabling it sets the gate to 0 -- confirm the operator actually wants that before saving. To configure the reference check, pass --reference-check-json '{"enabled": true, "count": 2}' (count 1-5) -- it overrides any reference_check already present in --stages-json. If neither is given, the job's existing saved reference_check is preserved as-is; omitting both does NOT disable an already-configured reference check. Prefer threading an approved draft into hire run --stage-plan-json (manage_jobs) at role-creation time instead, so the plan is persisted atomically with the job; use hiring-flow set to change the flow (or just the reference check) on an already-created job. Always confirm the stages with the operator before calling hiring-flow set -- it overwrites the saved plan and can change the live auto-invite gate.
Allowed commands
hiring-flow show, hiring-flow draft, hiring-flow set
Reading the saved company profile¶
| Tool ID | read_company_profile |
| Access | Read-only |
Check what's already saved on the caller's own employer profile (company name, website, logo, and company_information) before asking the operator for any of it. Use command: "clients profile". Writes nothing. ZER-2462/ZER-2463: never ask the operator to retype a company name or website that's already on file -- call this first, every time first contact comes up, including in a brand-new conversation with an employer you've talked to before; only ask for or generate whatever comes back empty.
Allowed commands
clients profile
Previewing a company profile¶
| Tool ID | propose_company_profile |
| Access | Read-only |
Preview a company name and website before saving them to the caller's own employer profile. Use command: "clients propose-company-profile" with --company-name and --website-url. Makes no backend call and saves nothing -- it only normalizes and echoes the values back so they can be read out to the operator for confirmation -- it performs no lookup of its own, so it can never fill in a company name you don't already have (resolve that with generate_company_info rather than asking the operator to type it). Check read_company_profile first: only run this for whichever of name/website isn't already saved.
Allowed commands
clients propose-company-profile
Saving a company profile¶
| Tool ID | confirm_company_profile |
| Access | Write |
Save the company name and website staged by propose_company_profile to the caller's own employer profile. Use command: "clients confirm-company-profile" with the SAME --company-name and --website-url values just previewed -- call this only immediately after propose_company_profile and an explicit, unambiguous yes to that exact preview, in the same exchange. A verbal yes is not itself a save -- nothing is persisted until this call executes. Its response may include a logo_url (whatever is currently on file, if anything); if one comes back, mention it to the operator as part of confirming what was saved, and never ask them for a logo URL -- there is no argument for one here. Also accepts an optional --company-information (see generate_company_info) in the SAME call to save that too. The backend resolves the employer from the caller's own identity -- this tool has no employer-id argument and can only ever write the caller's own profile, never another employer's.
Allowed commands
clients confirm-company-profile
Drafting a company description from the website¶
| Tool ID | generate_company_info |
| Access | Read-only |
Draft company_information (the company description/FAQ text the AI interviewer uses to answer candidate questions) from the employer's website. Use command: "clients generate-company-info" with --website-url. Makes no write -- pass the result to confirm_company_profile's --company-information to save it. The first-contact profile isn't fully captured until this is saved, not just the name and website. This is ALSO how you resolve the company name from a website instead of asking the operator to type it: the description is written from their real site and names the company, so read the canonical name out of it and pass that to propose_company_profile. So run this whenever company_information is empty OR the company name is still unknown, and run it only once per website -- one call yields both, and both go into the same confirm_company_profile write.
Allowed commands
clients generate-company-info
Install¶
See the MCP server README for Node requirements, Claude Desktop .mcpb bundles, and HTTP transport setup.