Get Application Review Details
<usecase>
Get the full state of one Application Review: the ACTIVE ICP, any pending DRAFT edit, the RANKING version if a rerank is in flight, the version history, evaluation status, a decision/calibration summary, and the fit-band distribution of the current ranking.
This is the tool to read before refining an ICP. It tells you what the review is currently scoring against, whether an edit is already pending or a rerank is running (so you know to wait), and how candidates are distributed across fit bands (so you can judge whether the bar is where you want it).
Pair with list_application_review_candidates to inspect individual ranked applicants.
</usecase>
<instructions>
Args:
application_review_id: The review to inspect.
detail_level: How much data to return.
'full' — (default) the ICP text, version history, evaluation status, decisions, and fit distribution.
'concise' — run and rerank state, active/ranking display versions, pending counts, and next-step guidance. Use this for cheap polling after an ICP update or restore.
icp_version: Return the full ICP text of a specific historical version instead of only the summary. This is a display version number (1, 2, 3, ...) as shown in version_history and the web app, not a raw internal id. Omit to get the ACTIVE / DRAFT / RANKING texts. Use version_history in the response to discover which versions exist and to pick a restore target for restore_application_review_icp.
Version numbers are the positional numbers users see in the web app: approved versions numbered 1, 2, 3, ... oldest first. A pending draft is not numbered. icp_version is only available at detail_level='full'.
</instructions>
<response_format>
Full (default):
{"application_review_id": "550e8400-...", "job_title": "Senior PM, Travel Booking", "run_state": "ACTIVE", "icp_update_supported": true, "active_icp": {"version": 3, "text": "# Ideal Candidate Profile...", "activated_at": "2025-01-16T09:00:00Z", "activated_by": "Alice Smith"}, "pending_draft": null, "ranking_version": null, "version_history": [{"version": 3, "lifecycle_state": "ACTIVE", "change_source": "user_edit", "reasoning": "Raised the seniority bar", "created_at": "2025-01-16T08:55:00Z", "activated_at": "2025-01-16T09:00:00Z"}], "evaluation": {"in_progress": false, "evaluating_since": null}, "decisions": {"progressed": 12, "rejected": 30, "unreviewed": 97}, "pending_admission_count": 3, "pending_evaluation_count": 0, "fit_distribution": {"great": 14, "good": 40, "okay": 55, "no_evidence": 8, "poor": 25}, "next": "Read the active ICP, then refine it..."}
Concise:
{"application_review_id": "550e8400-...", "run_state": "ACTIVE", "active_version": 3, "ranking_version": 4, "rerank_in_flight": true, "pending_admission_count": 3, "pending_evaluation_count": 0, "next": "A full rerank is in progress..."}
</response_format>
<notes>
evaluation.in_progress is true whenever the review holds the evaluation lock. When a ranking_version is present a full rerank is running and its results are not yet readable; when it is absent the lock reflects routine incremental scoring of new applicants and the ACTIVE ranking remains stable.
fit_distribution counts admitted candidates per fit band for the ACTIVE version — the aggregate to watch across ICP edits. It contains no per-candidate ranking data.
pending_admission_count is applicants matched but not yet admitted onto the roster; pending_evaluation_count is admitted candidates not yet scored against the active version. decisions counts only admitted candidates (progressed / rejected / unreviewed).
icp_update_supported tells you whether update_application_review_icp and restore_application_review_icp will work on this review — some reviews can only have their ICP managed in the Metaview web app. All read tools work regardless.
</notes>
<examples>
Inspect a review before refining its ICP:
get_application_review_details(application_review_id="550e8400-...")
Poll a rerank without re-reading the full ICP:
get_application_review_details(application_review_id="550e8400-...", detail_level="concise")
Read the full text of an earlier ICP version (display number):
get_application_review_details(application_review_id="550e8400-...", icp_version=2)
</examples>
<data_model>
- An APPLICATION REVIEW (a REVIEW) evaluates inbound applicants for one ATS job against an ICP (Ideal Candidate Profile). Applicants who match the review's configured ATS stage are admitted, scored against the ICP, and ranked. A review is tied to one or more ATS job ids and can be active or archived.
- An ICP is the Ideal Candidate Profile the review scores applicants against. It is versioned through a lifecycle: DRAFT (a proposed edit, not yet applied) -> RANKING (approved and being applied, a full rerank of every admitted candidate is in flight) -> ACTIVE (live, the version candidates are currently scored against) -> ARCHIVED (a superseded former ACTIVE version). At most one DRAFT, one RANKING, and one ACTIVE version exist per review at any time.
- ICP version numbers are the positional numbers users see in the web app: the approved versions (RANKING / ACTIVE / ARCHIVED, never DRAFT) numbered 1, 2, 3, ... oldest first. A pending DRAFT is not numbered — it is surfaced as pending_draft, not as a version. All version numbers in these tools speak this display numbering, so the agent and the human see the same numbers.
- Each version records its change_source provenance (agent_suggestion = proposed by the in-app review agent; user_edit = written by a person or via MCP; restore = a rollback to an earlier version) and, once activated, who activated it and when.
- Applying an ICP edit triggers a FULL RERANK: a Step Functions run re-scores every admitted candidate against the new version. While it runs, the review holds an evaluation lock (evaluating_since is set) and a RANKING version exists; on completion the RANKING version becomes ACTIVE and the previous ACTIVE version is ARCHIVED. Reranks are free (no credits are spent) but heavyweight, and take time for large reviews.
- RESTORING a previous version (only ARCHIVED versions can be restored) mints a NEW latest version with the source's content rather than reactivating the old row. Prior evaluations against that content are carried over, so the rerank after a restore is typically fast — only candidates the source version never saw are re-scored. A restore is itself reversible: the replaced version stays in the history and can be restored back.
- Separately, INCREMENTAL EVALUATION scores newly-synced ATS applicants onto the existing ACTIVE version. This also sets the evaluation lock but creates no RANKING version; the ACTIVE ranking being read stays stable throughout.
- ADMISSION is review-local: a candidate is ADMITTED (part of the ranked roster, debited one credit at admission) or PENDING (matched but not yet admitted onto the review). The candidate-listing tool returns only the ADMITTED roster — the candidates a rerank actually scores; pending applicants are reported as an aggregate count, not as rows.
- FIT is reported only as a coarse band (FitLevel: great, good, okay, no_evidence, poor). The underlying numeric scores and ordinal rank positions are internal and are never exposed.
- A human DECISION on an admitted candidate is PROGRESSED (advanced) or REJECTED, or absent (undecided / null), optionally with free-text feedback. These are the values the candidate-listing tool returns and its decision_filter accepts (lowercased); the review-details calibration summary counts the same undecided bucket as 'unreviewed'. Decisions can sync back to the ATS (stage moves, rejection emails).
- Some reviews support updating their ICP via MCP and some do not (their ICP is managed only in the Metaview web app). get_application_review_details reports icp_update_supported for each review; all read tools work regardless.
</data_model>
List Application Review Candidates
<usecase>
List the review's ADMITTED (ranked) roster, ranked against the ACTIVE ICP.
This lists only candidates admitted onto the review — the roster a rerank actually scores. Applicants matched but not yet admitted are excluded and reported separately as pending_admission_count; admitted candidates not yet scored are reported as pending_evaluation_count.
This is the feedback half of ICP refinement: after an ICP update reranks the review, use this to verify the bar moved the way you intended. Candidates carry only review-specific facts — the coarse fit band, human decision, the agent's fit reasoning, and (at detail_level=full) non-fraud AI column results.
IMPORTANT: fit is reported only as a coarse band (great / good / okay / no_evidence / poor). Numeric scores and ordinal rank positions are never exposed, and the list is never ordered by internal score — it is ordered by fit band, then application date.
For a candidate's full profile, resume, or ATS application detail, call fetch_candidates with the candidate_id and this application_review_id — that tool owns the deep candidate surface; this one owns only what the review knows.
</usecase>
<instructions>
Args:
application_review_id: The review to list candidates for.
limit: Maximum candidates to return (default 20, max 100).
offset: Pagination offset (default 0).
detail_level: How much data to return per candidate.
'minimal' — candidate_id, name, fit band, decision, applied_at.
'summary' — (default) minimal plus decision metadata (decided_at, feedback) and the agent's fit reasoning summary.
'full' — summary plus non-fraud AI column results (column, value, fit, reasoning).
decision_filter: Filter by human decision — 'all' (default), 'progressed', 'rejected', or 'undecided'. The per-candidate 'decision' field is returned as PROGRESSED, REJECTED, or null (undecided); pass the lowercase form to filter ('undecided' matches a null decision).
fit_filter: Filter by fit band — one of great, good, okay, no_evidence, poor. Omit for all bands.
</instructions>
<response_format>{"application_review_id": "550e8400-...", "active_version": 3, "candidates": [{"candidate_id": "c1a2b3d4-...", "name": "Jane Smith", "overall_fit": "great", "decision": "PROGRESSED", "applied_at": "2025-01-14T09:00:00Z", "decided_at": "2025-01-16T11:00:00Z", "decision_feedback": "Strong product sense", "reasoning": "Led two 0-to-1 launches in travel..."}], "total_count": 142, "has_more": true, "pending_admission_count": 5, "pending_evaluation_count": 0, "next": "..."}</response_format>
<notes>Only completed rankings are served. If a full rerank is in flight (a RANKING version exists), this tool returns an error directing you to poll get_application_review_details until the new version is ACTIVE — half-evaluated results would corrupt the refine/verify loop. If routine incremental evaluation of new applicants is running (no RANKING version), the stable ACTIVE ranking is served with pending_evaluation_count noting how many candidates are not yet scored. The response echoes active_version — the ACTIVE ICP version number (the same display number shown in the web app and get_application_review_details) the returned fit bands were computed against — so you can correlate bands to a specific ICP and detect drift without a second call (null when the review has no active ranking yet).</notes>
<examples>
List the top candidates for a review:
list_application_review_candidates(application_review_id="550e8400-...")
Check who was progressed:
list_application_review_candidates(application_review_id="550e8400-...", decision_filter="progressed")
Inspect only great-fit candidates with full column detail:
list_application_review_candidates(application_review_id="550e8400-...", fit_filter="great", detail_level="full")
</examples>
<data_model>
- An APPLICATION REVIEW (a REVIEW) evaluates inbound applicants for one ATS job against an ICP (Ideal Candidate Profile). Applicants who match the review's configured ATS stage are admitted, scored against the ICP, and ranked. A review is tied to one or more ATS job ids and can be active or archived.
- An ICP is the Ideal Candidate Profile the review scores applicants against. It is versioned through a lifecycle: DRAFT (a proposed edit, not yet applied) -> RANKING (approved and being applied, a full rerank of every admitted candidate is in flight) -> ACTIVE (live, the version candidates are currently scored against) -> ARCHIVED (a superseded former ACTIVE version). At most one DRAFT, one RANKING, and one ACTIVE version exist per review at any time.
- ICP version numbers are the positional numbers users see in the web app: the approved versions (RANKING / ACTIVE / ARCHIVED, never DRAFT) numbered 1, 2, 3, ... oldest first. A pending DRAFT is not numbered — it is surfaced as pending_draft, not as a version. All version numbers in these tools speak this display numbering, so the agent and the human see the same numbers.
- Each version records its change_source provenance (agent_suggestion = proposed by the in-app review agent; user_edit = written by a person or via MCP; restore = a rollback to an earlier version) and, once activated, who activated it and when.
- Applying an ICP edit triggers a FULL RERANK: a Step Functions run re-scores every admitted candidate against the new version. While it runs, the review holds an evaluation lock (evaluating_since is set) and a RANKING version exists; on completion the RANKING version becomes ACTIVE and the previous ACTIVE version is ARCHIVED. Reranks are free (no credits are spent) but heavyweight, and take time for large reviews.
- RESTORING a previous version (only ARCHIVED versions can be restored) mints a NEW latest version with the source's content rather than reactivating the old row. Prior evaluations against that content are carried over, so the rerank after a restore is typically fast — only candidates the source version never saw are re-scored. A restore is itself reversible: the replaced version stays in the history and can be restored back.
- Separately, INCREMENTAL EVALUATION scores newly-synced ATS applicants onto the existing ACTIVE version. This also sets the evaluation lock but creates no RANKING version; the ACTIVE ranking being read stays stable throughout.
- ADMISSION is review-local: a candidate is ADMITTED (part of the ranked roster, debited one credit at admission) or PENDING (matched but not yet admitted onto the review). The candidate-listing tool returns only the ADMITTED roster — the candidates a rerank actually scores; pending applicants are reported as an aggregate count, not as rows.
- FIT is reported only as a coarse band (FitLevel: great, good, okay, no_evidence, poor). The underlying numeric scores and ordinal rank positions are internal and are never exposed.
- A human DECISION on an admitted candidate is PROGRESSED (advanced) or REJECTED, or absent (undecided / null), optionally with free-text feedback. These are the values the candidate-listing tool returns and its decision_filter accepts (lowercased); the review-details calibration summary counts the same undecided bucket as 'unreviewed'. Decisions can sync back to the ATS (stage moves, rejection emails).
- Some reviews support updating their ICP via MCP and some do not (their ICP is managed only in the Metaview web app). get_application_review_details reports icp_update_supported for each review; all read tools work regardless.
</data_model>
List Application Reviews
<usecase>
List the Application Reviews you can access, with summary information per review.
An Application Review evaluates inbound applicants for a job against an ICP (Ideal Candidate Profile). Use this to find a review to inspect or refine, then call get_application_review_details for its ICP, version history, and evaluation status, or list_application_review_candidates for the ranked applicants.
Each review reports its ATS job, run state, active ICP version summary, whether a pending ICP draft exists, whether a rerank is currently in flight, and admitted / pending candidate counts.
</usecase>
<instructions>
Args:
include_archived: Include archived reviews (default false).
search_term: Case-insensitive substring match on the job title. Omit to list all.
limit: Maximum number of reviews to return (default 20, max 50).
offset: Pagination offset (default 0).
Results are scoped to reviews you can access via ATS hiring-team membership, admin access, or an explicit share.
</instructions>
<response_format>{"reviews": [{"id": "550e8400-...", "job_title": "Senior PM, Travel Booking", "ats_job_ids": ["job_123"], "run_state": "ACTIVE", "active_version": {"version": 3, "lifecycle_state": "ACTIVE", "activated_at": "2025-01-16T09:00:00Z"}, "has_pending_draft": false, "rerank_in_flight": false, "admitted_count": 142, "pending_count": 3, "created_at": "2025-01-10T10:30:00Z", "updated_at": "2025-01-16T14:22:00Z"}], "total_count": 8, "has_more": false}</response_format>
<notes>rerank_in_flight is true when a RANKING version exists (a full rerank triggered by an ICP update is running). While it is true, candidate rankings for this review are still being computed — list_application_review_candidates will decline to serve partial results; poll get_application_review_details until it clears.</notes>
<examples>
List active reviews:
list_application_reviews()
Find a review by job title:
list_application_reviews(search_term="Travel Booking")
Include archived reviews and paginate:
list_application_reviews(include_archived=True, limit=50, offset=50)
</examples>
<data_model>
- An APPLICATION REVIEW (a REVIEW) evaluates inbound applicants for one ATS job against an ICP (Ideal Candidate Profile). Applicants who match the review's configured ATS stage are admitted, scored against the ICP, and ranked. A review is tied to one or more ATS job ids and can be active or archived.
- An ICP is the Ideal Candidate Profile the review scores applicants against. It is versioned through a lifecycle: DRAFT (a proposed edit, not yet applied) -> RANKING (approved and being applied, a full rerank of every admitted candidate is in flight) -> ACTIVE (live, the version candidates are currently scored against) -> ARCHIVED (a superseded former ACTIVE version). At most one DRAFT, one RANKING, and one ACTIVE version exist per review at any time.
- ICP version numbers are the positional numbers users see in the web app: the approved versions (RANKING / ACTIVE / ARCHIVED, never DRAFT) numbered 1, 2, 3, ... oldest first. A pending DRAFT is not numbered — it is surfaced as pending_draft, not as a version. All version numbers in these tools speak this display numbering, so the agent and the human see the same numbers.
- Each version records its change_source provenance (agent_suggestion = proposed by the in-app review agent; user_edit = written by a person or via MCP; restore = a rollback to an earlier version) and, once activated, who activated it and when.
- Applying an ICP edit triggers a FULL RERANK: a Step Functions run re-scores every admitted candidate against the new version. While it runs, the review holds an evaluation lock (evaluating_since is set) and a RANKING version exists; on completion the RANKING version becomes ACTIVE and the previous ACTIVE version is ARCHIVED. Reranks are free (no credits are spent) but heavyweight, and take time for large reviews.
- RESTORING a previous version (only ARCHIVED versions can be restored) mints a NEW latest version with the source's content rather than reactivating the old row. Prior evaluations against that content are carried over, so the rerank after a restore is typically fast — only candidates the source version never saw are re-scored. A restore is itself reversible: the replaced version stays in the history and can be restored back.
- Separately, INCREMENTAL EVALUATION scores newly-synced ATS applicants onto the existing ACTIVE version. This also sets the evaluation lock but creates no RANKING version; the ACTIVE ranking being read stays stable throughout.
- ADMISSION is review-local: a candidate is ADMITTED (part of the ranked roster, debited one credit at admission) or PENDING (matched but not yet admitted onto the review). The candidate-listing tool returns only the ADMITTED roster — the candidates a rerank actually scores; pending applicants are reported as an aggregate count, not as rows.
- FIT is reported only as a coarse band (FitLevel: great, good, okay, no_evidence, poor). The underlying numeric scores and ordinal rank positions are internal and are never exposed.
- A human DECISION on an admitted candidate is PROGRESSED (advanced) or REJECTED, or absent (undecided / null), optionally with free-text feedback. These are the values the candidate-listing tool returns and its decision_filter accepts (lowercased); the review-details calibration summary counts the same undecided bucket as 'unreviewed'. Decisions can sync back to the ATS (stage moves, rejection emails).
- Some reviews support updating their ICP via MCP and some do not (their ICP is managed only in the Metaview web app). get_application_review_details reports icp_update_supported for each review; all read tools work regardless.
</data_model>
Restore Application Review Icp
<usecase>
Restore a previous Application Review ICP version: the restored content becomes a new latest version, is approved on the spot, and a rerank starts against it.
IMPORTANT: this applies immediately — there is no separate confirmation or approval step. The rerank after a restore is typically fast: prior evaluations against the restored content are reused, so only candidates that version never saw are re-evaluated.
Always call get_application_review_details first and pick the target from version_history — the version argument is the same display number (1, 2, 3, ...) shown there and in the web app. version_history carries only summaries; to read the full ICP text of a candidate version before restoring it, call get_application_review_details again with icp_version set to that display number. Only a previous (superseded) version can be restored: the currently active version and a version mid-ranking are rejected.
If the review has a pending, unapplied ICP draft, this restore supersedes it — exactly as restoring in the Metaview web app does.
A restore is itself reversible — the replaced version stays in version_history, so you can restore back to it.
</usecase>
<instructions>
Args:
application_review_id: The review to restore a version on (from list_application_reviews).
version: The display version number to restore, as shown in version_history from get_application_review_details (approved versions numbered 1, 2, 3, ... oldest first). Inspect a version's full text first via get_application_review_details(icp_version=N).
Some reviews can only have their ICP managed in the Metaview web app; a restore against one is rejected. If a rerank is already running, the restore is rejected — poll for it to finish, then retry.
</instructions>
<response_format>{"application_review_id": "550e8400-...", "restored_version": 2, "status": "approved_and_ranking", "next": "Restore approved and a rerank started..."}</response_format>
<examples>
Read a past version's full text, then roll back to it:
get_application_review_details(application_review_id="550e8400-...", icp_version=2)
restore_application_review_icp(application_review_id="550e8400-...", version=2)
</examples>
Update Application Review Icp
<usecase>
Replace an Application Review's ideal candidate profile (ICP) and immediately rerank every admitted candidate against the new profile.
IMPORTANT: this is not a draft or a suggestion. The ICP text you pass is written as a user edit, approved on the spot, and triggers a full rerank of all admitted candidates. There is no separate confirmation or approval step — the moment you call this tool, the change is live. Only call it with complete, final ICP text you are ready to apply. The edit is reversible — restore a prior ICP version with restore_application_review_icp.
Always call get_application_review_details first: read the current ICP, compose the full revised profile, and pass the current active_icp version number as base_version so a concurrent edit isn't silently clobbered.
If the review has a pending, unapplied ICP draft, this edit supersedes it — exactly as approving an edit in the Metaview web app does.
Reranks consume no credits (already-admitted candidates are not re-debited) but are heavyweight and can take a while on large reviews. After calling this, poll get_application_review_details with detail_level='concise' — the version moves RANKING then ACTIVE when evaluation completes. Candidate listings via list_application_review_candidates are unavailable until it completes. A further ICP update is rejected while that rerank is in flight — wait for it to finish before editing again.
</usecase>
<instructions>
Args:
application_review_id: The review to update (from list_application_reviews).
icp_text: The complete, final ICP text. This replaces the current profile in full — do not send a diff or a partial edit.
base_version: The active ICP version number you read from active_icp.version in get_application_review_details (the display number shown in the web app). If it no longer matches the review's current version (someone edited it since you read it), the update is rejected and you must re-read and re-apply.
Some reviews can only have their ICP managed in the Metaview web app; an update against one is rejected. If a rerank is already running, the update is rejected — poll for it to finish, then retry.
</instructions>
<response_format>{"application_review_id": "550e8400-...", "submitted_version_base": 3, "status": "approved_and_ranking", "next": "Full rerank started..."}</response_format>
<examples>
Refine an ICP after reading its current version:
update_application_review_icp(application_review_id="550e8400-...", base_version=3, icp_text="Senior PM with 6+ years in travel/booking marketplaces, strong on pricing experimentation...")
</examples>
Fetch Candidates
<usecase>
ALWAYS use this tool to look up one or more people or candidates. This is
the ONLY way to retrieve candidate scorecards, ATS feedback, resume files,
application history, and professional profile data. Do NOT try to scrape
LinkedIn or other websites directly — this tool fetches richer data than
any public page and includes internal ATS records.
Look up candidates by candidate ID, LinkedIn URL, email, phone number, or participant ID.
Returns per-candidate results with two sections each:
- profile: Professional background (experience, education, skills,
location, etc.). Available when a LinkedIn URL is provided or can be
resolved from ATS data.
- ats: Internal ATS data including:
- scorecards: Interview scorecard responses (questions and answers/scores)
- applications: Job applications with title, status, and stage
- feedback: Interviewer feedback notes
- files: Resumes and other attached documents with download URLs
Only available when the candidate exists in your ATS and you have
permission to access their data.
Use this tool when you need to:
- Look up scorecard results for one or more candidates
- Get a candidate's application status or interview stage
- Retrieve resume or document files for a candidate
- Get a candidate's professional profile (experience, education, skills)
- Check interviewer feedback on a candidate
ATS data access requires one of:
- You are a Metaview admin
- You are an ATS admin
- The candidate appeared in a conversation you have access to
- You provide an application_review_id for an accessible review whose admitted active roster includes the candidate
Application Review-based access grants the generic candidate applications/files ATS shape while withholding scorecards and global ATS feedback. The profile section remains the generic professional-profile surface. Access is review-wide, matching the web app for multi-job reviews, and remains available during reranks because this data is independent of ranking state.
Accepts 1-10 candidates per call.
</usecase>
<instructions>
Args:
candidates: A list of candidate lookups. Each entry is a dict with
at least one identifier:
- candidate_id: Metaview candidate UUID for direct lookup. This is the
candidate_id returned by the application review tools (list_application_review_candidates), so a review listing pivots
straight to a full profile.
- application_review_id: Application Review UUID that authorizes access to the
candidate. Requires candidate_id; the candidate must be on that accessible
review's admitted active roster.
- linkedin_url: LinkedIn profile URL (e.g. "https://www.linkedin.com/in/john-doe")
or bare shorthand (e.g. "john-doe").
- email: Candidate email address.
- phone_number: Candidate phone number.
- participant_id: Metaview participant ID for direct lookup. This is the
internal ID shown in conversation data (e.g. from the default:candidate
field in search_conversations results).
</instructions>
<response_format>
{"candidates": [
{
"lookup": {"email": "john@example.com"},
"profile": {
"name": "John Doe",
"linkedin_url": "https://www.linkedin.com/in/john-doe",
"location": "San Francisco, CA", ...},
"ats": {
"name": "John Doe",
"applications": [{"job_title": "...", "status": "...", "stage": "..."}],
"scorecards": [{"question": "...", "answer_or_score": "..."}],
"feedback": ["..."],
"files": [{"file_name": "resume.pdf", "download_url": "..."}]}
},
{
"lookup": {"linkedin_url": "https://www.linkedin.com/in/jane-smith"},
"error": "No candidate found matching the provided identifiers."
}
]}
</response_format>
<examples>
Fetch a single candidate by email:
fetch_candidates(candidates=[{"email": "john@example.com"}])
Fetch a candidate by candidate_id:
fetch_candidates(candidates=[{"candidate_id": "c1a2b3d4-..."}])
Fetch multiple candidates:
fetch_candidates(candidates=[
{"linkedin_url": "https://www.linkedin.com/in/john-doe"},
{"email": "jane@example.com"},
{"participant_id": "abc-123-def"}])
Fetch with multiple identifiers per candidate (for better matching):
fetch_candidates(candidates=[
{"linkedin_url": "https://www.linkedin.com/in/john-doe", "email": "john@example.com"}])
Fetch a candidate from an Application Review:
fetch_candidates(candidates=[{"candidate_id": "c1a2b3d4-...", "application_review_id": "550e8400-..."}])
</examples>
<access_scope>Results are scoped server-side to interviews and candidates the authenticated user can already access in Metaview, within their workspace and role. Intended for the user's normal hiring workflow (reviewing, summarizing, exporting, ATS sync). Read-only.</access_scope>
List Note Templates
<usecase>
List AI Notes custom templates the caller can access.
Returns a lightweight summary per template. Pass include_detail=true or call get_note_template for the full sections.
</usecase>
<instructions>
Args:
search_term: Filter templates by name (case-insensitive substring /
regex).
include_detail: When true, include the full sections list.
offset: Pagination offset (default 0).
limit: Page size (default 20, max 50).
</instructions>
<response_format>
{"templates": [
{"id": "...", "name": "Engineering screen", "version": 3,
"created_by": {"name": "Alice Smith"},
"created_at": "...", "updated_at": "...",
"section_count": 5}],
"total_count": 12, "has_more": false}
The `id` field is INTERNAL ONLY — keep it for subsequent tool calls
but never show it to the user. Refer to templates by name.
</response_format>
<data_model>
- A NOTE TEMPLATE (AI Notes custom template) defines the structure of AI-generated interview notes. Each template has a name and an ordered list of sections. Template ids and group ids are internal — never surface them to the user; refer to both by name.
- A SECTION is one named block in the generated notes — it has a directive (SINGLE for one block under a fixed heading, or REPEATING for one block per detected entity), a description (the instruction the LLM follows when writing that block — the single most important field), and an output type (TEXT, NUMBER, OPTIONS, or DATE).
- A TEMPLATE GROUP (folder) is a public folder shared with the workspace. Templates live in the caller's personal folder by default or in a public template group. Use list_note_template_groups to see public folders, and manage_note_template to create/update/delete a template.
- A SOURCE is a piece of content that feeds into AI Notes generation. Sources can be conversations (type=session — an interview transcript) or documents (type=document — a resume, job description, or other plain text). Notes always have an anchor conversation (the one passed to generate_notes). Additional sources can be added or removed via manage_notes_sources.
</data_model>
Get Note Template
<usecase>
Fetch a single AI Notes custom template with its full section configuration.
Use list_note_templates first if you don't have a template ID.
</usecase>
<instructions>
Args:
template_id: Required. UUID of the template. INTERNAL ONLY — never
show this id to the user; refer to templates by name.
</instructions>
<response_format>
{"template": {"id": "...", "name": "...", "version": 3,
"created_by": {"name": "Alice Smith"},
"created_at": "...", "updated_at": "...",
"section_count": 2, "sections": [...]}}
The `id` is INTERNAL ONLY — never show it to the user.
</response_format>
List Note Template Groups
<usecase>
List the public folders (template groups) in the caller's workspace.
Use this when the user wants to move a template into a named folder —
call list_note_template_groups first to resolve the folder name,
then pass it to manage_note_template(action='update', group_name='...').
Templates not in any public folder live in the caller's personal folder.
</usecase>
<instructions>Args: (none)</instructions>
<response_format>
{"groups": [
{"id": "...", "name": "Engineering", "template_count": 4},
{"id": "...", "name": "Recruiter", "template_count": 7}]}
The `id` field is INTERNAL ONLY — never show it to the user. Refer to
folders by name.
</response_format>
Generate Notes
<usecase>
Generate (or regenerate) AI Notes for a conversation, optionally with a specific template.
Use this tool to:
- Trigger notes generation for a conversation that has no notes yet.
- Regenerate notes using a different template (built-in or custom).
- Preview how a newly created or updated template renders on a real conversation.
Regenerating creates a new version of the conversation's notes; prior versions are preserved and the user can revert to them in the Metaview web app.
</usecase>
<instructions>
Args:
conversation_id: The conversation (session) ID to generate notes for.
template: Optional. Built-in template options:
'question_and_answer' (Question and Answer)
'conversational' (Topic Highlights)
'recruiter_screen' (Recruiter Screen)
'generic_debrief' (Generic Debrief)
'team_role_debrief' (Team-Specific Debrief)
'technical_role_debrief' (Technical Debrief)
'intake_call' (Intake | Role Scope)
'candidate_pack' (Candidate Pack)
'coding_interview' (Coding Interview)
'system_design_interview' (System Design Interview)
'role_alignment' (Role Alignment)
'client_call' (Client Call)
Use 'custom_definition' with template_definition_id for custom templates.
Omit to auto-select the best template for the conversation.
template_definition_id: Optional. UUID of a custom note template.
Required when template='custom_definition'. Get the ID from
list_note_templates or get_note_template.
</instructions>
<response_format>{"status": "generation_queued", "message": "..."}</response_format>
<examples>
Generate with auto-selected template:
generate_notes(conversation_id="12345")
Generate with a built-in template:
generate_notes(conversation_id="12345", template="question_and_answer")
Generate with a custom template:
generate_notes(conversation_id="12345",
template="custom_definition",
template_definition_id="<uuid>")
</examples>
Manage Note Template
<usecase>Create, update, or delete an AI Notes custom template.</usecase>
<instructions>
Args:
action: Required. One of: create, update, delete.
template_id: Required for update and delete. INTERNAL ONLY — never
surface this id to the user (it's a UUID with no meaning to
them). Refer to templates by name in user-facing messages.
name: Required for create; optional for update.
sections: Required for create; optional for update. When provided
on update, the full list REPLACES the existing sections —
omitted sections are removed. List order determines display
order. See section_schema for the section shape.
group_name: Optional (update only). Move the template into a named
public folder in the caller's workspace. Pass an empty string
(or 'personal') to move into the personal folder. Call list_note_template_groups
first to see the available folder names. Omit the arg to leave
the template where it is.
</instructions>
<response_format>
Create / update: {"template": {template detail}}
Delete: {"deleted": true, "template_id": "..."}
The `template_id` / `template.id` fields are INTERNAL ONLY — retain them
for subsequent tool calls but never show them to the user. When confirming
a save or delete to the user, refer to the template by name.
</response_format>
<examples>
Create with one TEXT + one OPTIONS section. Each description carries
scope and a format contract — the LLM needs that level of specificity
on every interview transcript.
manage_note_template(action="create", name="Recruiter screen", sections=[
{"directive": "SINGLE", "heading": "Reasons for leaving",
"description": "Summarise the candidate's reasons for leaving their current role in 3-4 bullets. Each bullet: **Push** or **Pull** in bold, colon, one sentence. Quote the candidate's own phrases in italics.",
"output": {"type": "TEXT", "text_format": "BULLET_POINT", "detail": "medium"}},
{"directive": "SINGLE", "heading": "Remote preference",
"description": "Pick \"Remote\" if the candidate said remote-only, \"Hybrid\" if they mentioned office days or hybrid setups, \"In-office\" if they said they want to be in the office full time, \"Not discussed\" if the topic never came up.",
"output": {"type": "OPTIONS", "options_type": "SINGLE",
"options": ["Remote", "Hybrid", "In-office", "Not discussed"]}}])
REPEATING for 'one block per X' (capture a block per technical question,
not five near-identical SINGLE sections):
manage_note_template(action="create", name="Tech screen", sections=[
{"directive": "REPEATING", "repeating_entity": "Technical question",
"description": "For each technical question the interviewer asks: capture the question verbatim as a markdown heading, then one paragraph covering the candidate's approach and whether they arrived at a correct solution.",
"output": {"type": "TEXT", "text_format": "PARAGRAPH", "detail": "high"}}])
Rename only:
manage_note_template(action="update", template_id="abc", name="Renamed")
Delete:
manage_note_template(action="delete", template_id="abc")
</examples>
<section_schema>
A SECTION is one named block in the generated notes. Every field is required.
Shape:
directive: 'SINGLE' | 'REPEATING'
'SINGLE' renders one block under a fixed heading.
'REPEATING' renders one block per detected entity in the transcript.
Prefer REPEATING whenever the user's intent is 'one block per X'
(per question / per role / per project / per competency). If you
find yourself drafting several near-identical SINGLE sections
that differ only in which question or topic they cover, collapse
them into one REPEATING section with a clear repeating_entity.
heading: str (required when directive == 'SINGLE')
repeating_entity: str (required when directive == 'REPEATING';
e.g. 'Question', 'Topic', 'Role', 'Project')
description: str THE single most important field. Treat it as a
PROMPT to another LLM — not a note to a human
colleague. It runs at inference time on every
interview transcript. Cover two things:
(a) SCOPE — what to include and what to
leave out.
(b) OUTPUT FORMAT — the exact shape. Caps on
length (word / bullet / sentence count),
markdown patterns, label conventions.
e.g. '3 bullets max, each in the form
**Label:** one sentence. Total under
120 words. No tables.'
Encode any formatting preferences the user
has stated verbatim.
Examples:
SINGLE section:
'Summarise the candidate's reasons for
leaving their current role in 3-4 bullet
points. Each bullet: **Push** or **Pull**
in bold, colon, one sentence. Quote the
candidate's own phrases in italics.'
REPEATING section (directive=REPEATING,
repeating_entity='Technical question'):
'For each technical question asked: capture
the question verbatim as a markdown
heading, then a paragraph with the
candidate's approach and whether they
arrived at a correct solution.'
REPEATING descriptions are applied once per
detected entity.
output: dict (constrains the generated value; shape depends on type)
TEXT: {type: 'TEXT', text_format: 'PARAGRAPH'|'BULLET_POINT',
detail: 'low'|'medium'|'high'}
TEXT carries structure via markdown inside the
description — bold labels, sub-bullets, closed label
sets. Most production templates use TEXT for everything
that isn't a hard classification.
NUMBER: {type: 'NUMBER', currency: str}
(pass currency='' for a plain number; otherwise 'USD', 'EUR', ...)
OPTIONS: {type: 'OPTIONS', options_type: 'SINGLE'|'MULTIPLE',
options: ['Yes', 'No', ...]}
For booleans, use options=['true', 'false'] with
options_type='SINGLE'.
CRITICAL: an OPTIONS description must include per-option
selection criteria, not just the list. Tell the LLM
exactly when to pick each value and — just as
importantly — when NOT to. Example for
options=['Strong Yes','Yes','Mixed','No','Strong No']:
'Rate the candidate's impact evidence. Mixed is NOT a
hedge — use Mixed only when the interview contains
concrete evidence supporting both positive and
negative interpretations that cannot be resolved.
Execution alone is not impact: if the candidate
describes delivery without a validated outcome
(measured, observed, or confirmed change), the
rating must be No.'
Prefer 2-5 well-defined options. If you need more than
~6, you're usually classifying something the LLM can
describe in a TEXT section instead.
DATE: {type: 'DATE', date_format: 'yyyy/mm/dd'|'mm/dd/yyyy'|'dd/mm/yyyy'}
</section_schema>
<design_guidance>
## Design guidance
High-quality templates look like production LLM prompts, not polite notes
to a colleague. When iterating with the user on a new template:
- WRITE DESCRIPTIONS LIKE PROMPTS. Include scope and an explicit output
format in every description. Descriptions under ~200 characters almost
always produce vague notes.
- BIAS TOWARD REPEATING. If the user's intent is 'one block per X',
that's a REPEATING section — not 5 near-identical SINGLE sections.
Scripted case-study questions, per-project recaps, per-competency
assessments all belong in REPEATING.
- SPLIT vs MERGE. Split a concept into multiple sections when (a) the
outputs have different shapes, or (b) the user would want to iterate
on them independently. Keep them together when the output is
narrative prose about one topic.
- USE OPTIONS SPARINGLY. A 10+ value OPTIONS enum is usually a
classifier the LLM can handle inside a TEXT section description.
Reserve OPTIONS for ratings or decisions that have a small, fixed,
well-defined set of answers, and always include per-option criteria.
</design_guidance>
Manage Notes Sources
<usecase>
List, add, or remove sources on an existing AI Notes version.
Use this tool to:
- Inspect which conversations and documents are included in a notes version.
- Add extra conversations so the notes cover multiple interviews.
- Add a plain-text document (e.g. a resume, job description) as context.
- Remove a source that was previously added.
The notes must already exist — call generate_notes first if they do not.
</usecase>
<instructions>
Args:
conversation_id: The anchor conversation whose notes you want to manage.
This is the conversation that generate_notes was originally called with.
action: Required. One of: list, add, remove.
conversation_ids: Conversations to add or remove. List of conversation ID strings.
documents: Documents to add as plain text. Each entry is an object with:
- name: A short name for the document (e.g. "Resume - Jane Smith").
- content: The full text content of the document.
source_ids: Source IDs to remove (returned by a prior 'list' call).
Use this when removing document sources or when you already know the source ID.
IMPORTANT: Adding or removing sources triggers a full regeneration of the notes.
After add/remove, wait ~30 seconds then poll with search_conversations.
</instructions>
<response_format>
list: {"sources": [{"source_id": "...", "type": "session"|"document", "name": "..."}]}
add: {"status": "sources_added", "added_source_ids": [...]}
remove: {"status": "sources_removed", "removed_source_ids": [...]}
</response_format>
<examples>
List current sources:
manage_notes_sources(conversation_id="12345", action="list")
Add another conversation:
manage_notes_sources(conversation_id="12345", action="add",
conversation_ids=["67890"])
Add a document as plain text:
manage_notes_sources(conversation_id="12345", action="add",
documents=[{"name": "Resume - Jane Smith", "content": "Jane Smith..."}])
Remove a source by ID (from a prior list):
manage_notes_sources(conversation_id="12345", action="remove",
source_ids=["<uuid>"])
Remove a conversation source:
manage_notes_sources(conversation_id="12345", action="remove",
conversation_ids=["67890"])
</examples>
Create Ai Field
<usecase>
Create a new AI field or update an existing one.
AI fields are the primary tool for analyzing conversations at scale. Each field
defines a question that is answered independently for every conversation it is applied
to. This means you can extract specific data points from hundreds of conversations
without hitting context window limits — each conversation is analyzed in isolation,
and the structured results can then be aggregated, filtered, and grouped.
WHEN TO USE: When the user wants insights across 20+ conversations, create an AI field
to extract the specific data point, then use search_conversations or group_conversations
to aggregate the results. This is far more effective than trying to read all transcripts.
HELPING USERS APPROACH THE TASK: The tool automatically assesses and improves prompts
before creating the field (unless skip_improvement=true), so you do not need to help
the user craft the exact prompt — focus on helping them think through their approach:
- If the user's request is broad (e.g., 'analyze the interviews'), help them
identify what specific questions they want answered. What decisions are they
trying to make? What patterns are they looking for?
- If a single AI field won't capture what they need, suggest breaking it into
2-3 focused fields (e.g., one for candidate sentiment, one for key objections,
one for salary expectations) rather than one catch-all.
- Help them think about whether they need per-conversation detail (search) or
an aggregate view (group by interviewer, department, etc.).
IMPORTANT: Always confirm with the user before calling this tool. Describe the
field you plan to create or update (name, prompt, value type) and get explicit approval.
AI fields analyze every conversation they're used against, so the user should understand
what will be created or modified on their behalf.
</usecase>
<instructions>
Creating: When no field_id is provided, creates a new AI field. Before creating,
the tool searches for existing fields (both system-provided and workspace-custom) that
capture the same information. If a match is found, the existing field is returned instead
of creating a new one. If the suggested match is not what you need, retry with
skip_similarity_check=true to bypass the check and force creation.
Updating: When field_id is provided, updates the existing field's name, prompt,
and/or value_type. If the prompt, value_type, or name (for auto-prompted fields)
changes, existing results are cleared and re-analysis is triggered on next query.
AI fields are processed asynchronously. After creating or updating a field, use it in
`search_conversations` or `group_conversations` to retrieve results. If results
show `evaluation_pending`, retry after ~30 seconds.
Use list_fields(search_term=...) to check for existing AI fields
and prefab system fields before creating new ones — this tool also deduplicates
automatically, but searching first avoids the overhead.
Choosing a value_type:
Fields are most useful when their values can be filtered, grouped, and sorted in reports.
This means the output should be constrained to a known set of values whenever possible.
- Yes/no questions → use `boolean`
- Numeric scores or ratings → use `number` (e.g., 1-5 scale)
- Lists of items → use `list` instead of comma-separated text
- Categorical data → use `single_line_text` with the prompt constraining output to a fixed
set of allowed values (e.g., "Respond with exactly one of: None, Low, Moderate, High").
Never let `single_line_text` produce free-form text with explanations — it should act like
an enum so users can filter and group by exact values.
Only use free-form text (`single_line_text` without constrained values, or `long_text`) when
the answer is genuinely open-ended and cannot be represented as a number, boolean, list, or
category.
Args:
name: Human-readable field name (e.g., "Candidate Salary Expectation").
prompt: Instructions for what to extract from transcripts
(e.g., "Extract the candidate's salary expectations from the conversation").
value_type: The data type of the extracted value. One of: single_line_text, long_text, list, number, currency, date, boolean.
field_id: Optional. The field ID of an existing AI field to update (e.g., "AI:<uuid>").
Omit to create a new field.
skip_similarity_check: Optional. Set to true to skip the similarity check and force creation
of a new field. Use this when a previous call returned `already_existed: true` but the
suggested field does not meet your needs. Default false.
skip_improvement: Optional. Set to true to skip the automatic column improvement step.
By default, the tool assesses and improves the column definition (prompt, value_type)
before creation. Set to true to create the field exactly as specified. Default false.
Returns:
field_id: The field's ID for use in other tools (e.g., "AI:<uuid>").
name: The field name.
value_type: The field's value type.
already_existed: Whether an existing field was returned instead of creating a new one (create only).
similarity_explanation: When already_existed is true, explains why the existing field matched.
updated: Whether an existing field was updated (update only).
</instructions>
<performance>
IMPORTANT: Each AI field is analyzed per conversation. Analysis is asynchronous and
takes longer with more conversations. Always apply narrow filters (date range, department,
interviewer, etc.) in search_conversations or group_conversations
to limit analysis to the conversations you actually need. Broad unfiltered queries
will be slow and may hit evaluation limits.
</performance>
List Fields
<usecase>
List available fields for filtering, grouping, and columns, plus metrics.
Returns two SEPARATE lists:
1. fields — filter/grouping field metadata. Field IDs look like
"default:start_time", "OSPT:<uuid>", or "AI:<uuid>". Use these for
filters, group_by, columns, and the group parameter in
get_chart_data.
2. metrics — computed summaries you can chart or aggregate. Metric IDs
look like "aggregation:session_count", "aggregation:actual_duration",
or "aggregation:AI:<uuid>". Use these ONLY as metric_id in
group_conversations (with metrics) and
get_chart_data (chart_inputs).
Do NOT mix them up: field IDs go in filters/group_by/group,
metric IDs go in metric_id/chart_inputs.
Each metric includes chart_types showing which chart types it supports.
Only metrics with "scatter" in chart_types support scatter plots
(omitting function/interval in get_chart_data).
BEST PRACTICE: Use search_term to find specific fields instead of
fetching all. For example, use search_term="department" to find the department
field rather than retrieving the full list.
</usecase>
<instructions>
Args:
search_term: Filter fields by name (case-insensitive). Supports plain
substring matching and regex patterns (e.g. "eng.*|product" is
automatically detected as regex when it contains special characters).
report_id: Optional report ID to scope fields to a specific report's context.
</instructions>
<response_format>
{"fields": [
{"id": "default:start_time", "name": "Start Time", "type": "date",
"description": "When the conversation started",
"filterable": true, "groupable": false,
"displayable": true,
"filter_operations": ["before", "after", "between"],
"filter_format": "value MUST be a dict with ..."},
{"id": "OSPT:a1b2c3d4-e5f6-7890-abcd-ef1234567890", "name": "Department", "type": "string",
"description": null,
"filterable": true, "groupable": true,
"displayable": true,
"filter_operations": ["is_one_of", "is_not_one_of"],
"filter_format": "value: list of strings. Example: [\"Engineering\"]"},
...
],
"metrics": [
{"id": "aggregation:session_count", "name": "Conversation Count",
"functions": ["max", "mean", "median", "min", "sum"],
"chart_types": ["bar", "line"],
"chart_functions": ["Count", "Cumulative"]},
{"id": "aggregation:scorecard_submission_time",
"name": "Scorecard Submission Time", "unit": "minute",
"functions": ["max", "mean", "median", "min", "sum"],
"chart_types": ["bar", "line", "scatter"],
"chart_functions": ["Cumulative", "Max", "Mean", "Median", "Min", "Rolling Average", "Sum"]},
...
]}
Numeric metrics may include a "unit" field (e.g. "minute", "participant")
indicating the unit of the raw values. Always use this unit when
interpreting or presenting metric values to users.
</response_format>
<examples>
Find the department field:
list_fields(search_term="department")
Find date-related fields:
list_fields(search_term="date")
All fields for a specific report:
list_fields(report_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890")
All fields (no filter):
list_fields()
</examples>
<data_model>
- A CONVERSATION is a single recorded call (interview, demo, sync, etc.). Conversations can be RECORDED (Metaview captured and transcribed the call) or UNRECORDED (the call is on the calendar but was not recorded — e.g. the bot wasn't invited, the meeting was cancelled, or it hasn't happened yet). By default, queries only return recorded conversations. Pass only_show_recorded_conversations=false to include unrecorded ones.
- A USER is someone who has signed into Metaview with a work email. Users are identified by a numeric user_id.
- A CONTACT is a record of someone who appeared on a call. Contacts are identified by a UUID and are NOT unique per person — the same real-world person can have multiple contact records with different roles. For example, an employee who once interviewed at the company has one contact record as a candidate (linked to their personal email) and another as an interviewer (linked to their work email). This means searching by 'default:contact' may return duplicate results for the same person. To avoid this, always use role-scoped fields: 'default:interviewer' to find employees/interviewers, 'default:candidate' to find candidates.
- A FIELD is any data property on a conversation — things like department, interviewer name, duration, or an AI-extracted insight. Fields can be used to filter, group, display as columns, or chart conversations.
- A METRIC is a computed summary of a field's values across conversations — like the average, sum, or count. Metrics are used in groups (for breakdowns and rankings) and charts (for trends over time).
- A GROUP is a collection of conversations grouped by a field (e.g. by department, interviewer, or conversation type). Groups can include metrics like average duration, question count, or talk time — making them the primary tool for breakdowns, rankings, and comparisons.
- AI FIELDS (prefixed 'AI:') contain structured data extracted from transcripts by AI. They provide per-conversation insights without needing to read raw transcripts. Each AI field is analyzed per conversation, so applying filters focuses analysis on the conversations you care about and returns results faster. NOTE: In the Metaview web app, AI fields are displayed as 'AI Columns'. If the user refers to 'AI Columns', they mean AI fields.
- The 'default:transcript' field returns full transcript text when explicitly requested as a field. Transcripts are large — keep limit to 5 or fewer conversations when requesting transcripts. Prefer AI fields over raw transcripts when possible. On the Free plan, transcripts are only available for recent sessions within the monthly quota.
- The 'default:summary' field returns AI-generated summaries. Summaries are generated on the fly for conversations that don't have one yet. Generation is slow and capped at 100 per request. Each summary produces roughly 2,000 tokens per hour of conversation. Always narrow your query with tight filters before requesting summaries to avoid long waits and hitting the cap. On the Free plan, summaries are only available for recent sessions within the monthly quota.
</data_model>
<filtering_by_current_user>When the user asks about 'my' conversations, 'I' recorded, or anything self-referencing, filter using "$self" — it resolves to the authenticated user's participant ID automatically. Example: {"field_id": "default:interviewer", "operation": "includes_one_of", "value": ["$self"]}</filtering_by_current_user>
List Field Values
<usecase>
Get possible values for a specific field.
Use this to discover what values are available for filtering.
For example, get all department names, interviewer names, or job titles.
Essential for looking up person IDs needed by PERSON-type and PARTICIPANT-type filters.
BEST PRACTICE: Use search_term when looking for a specific value
(e.g., a person's name or a particular department). This returns fewer
results and avoids unnecessary data transfer.
Can be scoped to a report or filtered set of sessions to show only
values that appear in the matching sessions.
</usecase>
<instructions>
Contact lookup — choosing the right field:
The same person can have MULTIPLE contact records (e.g. one as an
interviewer and one as a candidate). To avoid duplicates:
- Looking up an interviewer / employee? Use "default:interviewer".
This returns only internal team members (has_access=True) — one result per person.
- Looking up a candidate? Use "default:candidate".
This returns only external participants (has_access=False).
- Don't know the role? Use "default:contact", but beware it may
return multiple records for the same person. Check the "is_internal" field
in the response to distinguish employees from candidates.
IMPORTANT: search_term supports substring matching ("Bart" matches "Bartels", "Bartosz", etc.) and regex matching (e.g. "eng.*team|product" is automatically detected as a regex pattern when the term contains special characters like .*+?[]|()). When multiple results are returned, disambiguate before proceeding. Prefer searching with both first and last name.
See search_conversations for the complete filter format reference. IMPORTANT: Date filter values must use a dict with scope and value, not a bare date string. Example: {"field_id": "default:start_time", "operation": "after", "value": {"scope": "relative", "value": -2592000}}
Args:
field_id: The field ID (from list_fields, e.g., "OSPT:<uuid>",
"default:interviewer").
report_id: Optional report ID to scope values to sessions in that report.
filters: Optional filters to scope values.
only_show_recorded_conversations: When true (default), only return recorded conversations. Set to false to also include unrecorded conversations (scheduled calls that were not recorded, or upcoming conversations that haven't happened yet).
search_term: Filter values by name/label. Supports substring and regex patterns.
offset: Pagination offset.
limit: Number of values per page (default 50).
</instructions>
<response_format>
For STRING-type fields, values are the actual strings:
{"field_id": "OSPT:<uuid>",
"values": [{"value": "Engineering", "label": "Engineering", "person": null},
{"value": "Product", "label": "Product", "person": null}],
"has_more": false}
For PERSON-type fields, values are integer IDs with name labels and person info:
{"field_id": "default:interviewer",
"values": [{"value": 123, "label": "Alice Smith",
"person": {"first_name": "Alice", "last_name": "Smith", "email": "alice@example.com"}},
{"value": 456, "label": "Bob Jones",
"person": {"first_name": "Bob", "last_name": "Jones", "email": "bob@example.com"}}],
"has_more": true}
For PARTICIPANT-type fields, values include an is_internal flag:
{"field_id": "default:contact",
"values": [{"value": "uuid-1", "label": "Alice Smith",
"person": {"first_name": "Alice", "last_name": "Smith", "email": "alice@example.com",
"is_internal": true}},
{"value": "uuid-2", "label": "Bob Jones",
"person": {"first_name": "Bob", "last_name": "Jones", "email": "bob@gmail.com",
"is_internal": false}}],
"has_more": false}
</response_format>
<examples>
List all departments (get the field ID from list_fields first):
list_field_values(field_id="OSPT:<uuid>")
Search for an interviewer by name (preferred for employees):
list_field_values(field_id="default:interviewer",
search_term="Alice Smith")
Search for a candidate by name:
list_field_values(field_id="default:candidate",
search_term="Bob Jones")
Values scoped to a saved report:
list_field_values(field_id="OSPT:<uuid>", report_id="abc-123")
</examples>
Get Chart Data
<usecase>
Get chart data for aggregate time-series or scatter plots.
Each chart_input is processed independently. The chart type is determined
automatically from the keys present:
- Aggregate chart (line/bar/stacked bar): include `function` AND
`interval` in the chart_input. Returns time-bucketed data points.
- Values chart (scatter plot): omit `function` and `interval`.
Returns one data point per conversation.
IMPORTANT: Not all metrics support scatter charts. Check the metric's
chart_types from list_fields. Only metrics with "scatter" in
chart_types can be used as scatter plots (omitting function/interval).
For example, "Conversation Count" only supports bar/line (aggregate)
charts, not scatter plots.
Use list_fields to discover valid metric_id values
and their supported chart types and aggregation functions.
</usecase>
<instructions>
See search_conversations for the complete filter format reference. IMPORTANT: Date filter values must use a dict with scope and value, not a bare date string. Example: {"field_id": "default:start_time", "operation": "after", "value": {"scope": "relative", "value": -2592000}}
Args:
chart_inputs: Array of chart specifications. Each dict must have:
metric_id (str, required) — the metric to chart.
Call list_fields to discover available IDs.
Metric IDs start with "aggregation:" (e.g. "aggregation:session_count",
"aggregation:actual_duration") or "AI:<uuid>" for AI fields.
function (str) — one of: Average, Median, Count, Sum,
Max, Min, Cumulative, Rolling Average. Required for aggregate charts,
must be omitted for scatter charts. Must be in the metric's
supported chart_functions (from list_fields).
interval (str) — one of: day, week, month, quarter. Required for
aggregate charts, must be omitted for scatter charts. Both
function and interval must be provided together or
both omitted.
metric_arguments (dict, optional) — extra arguments. Check
required_arguments from list_fields.
For contact-scoped metrics (question_count, talk_time, minutes_late) you MUST pass {{"scope": "internal"}} or {{"scope": "external"}}. "internal" = interviewers/hiring team, "external" = non-team participants (candidates, clients, etc.).
function_arguments (dict, optional) — for Rolling Average,
include {"windowIntervalCount": <int>}.
NOTE: For values (scatter) charts, omit function and interval.
report_id: ID of a saved report.
filters: Array of filter objects, each with field_id (str), operation (str), and value.
only_show_recorded_conversations: When true (default), only return recorded conversations. Set to false to also include unrecorded conversations (scheduled calls that were not recorded, or upcoming conversations that haven't happened yet).
group: Field ID to scope the chart to conversations that have a value for
this field (e.g. "OSPT:<uuid>" limits the chart to conversations
with a department set). This is a filter, NOT a breakdown — it does not
split the chart by group member. To get per-group metrics, use
group_conversations with metrics instead.
IMPORTANT: This must be a field ID (e.g. "OSPT:<uuid>",
"default:interviewer"), NOT a metric ID. Do not pass
"aggregation:..." IDs here.
min_count: Minimum conversations per group (when group is specified).
timezone_offset_minutes: Timezone offset for date bucketing (default 0 = UTC).
Only used by aggregate charts.
</instructions>
<response_format>
{"charts": [
{"metric_id": "aggregation:session_count",
"function": "Count",
"interval": "week",
"data": [
{"start_date": "2024-01-01", "value": 15.0,
"values": [{"label": null, "value": 15}]}
]},
{"metric_id": "aggregation:actual_duration",
"data": [
{"conversation_id": 12345, "date_time": "2024-01-15 10:00:00+00:00",
"value": 45.2, "label": "45 min"}
]}
]}
</response_format>
<notes>Note: AI field values are computed asynchronously — if "ai_processing" is present in the response, poll by retrying the same call every ~30 seconds. Always apply tight filters before using AI fields. See search_conversations for the full AI field evaluation guidance.</notes>
<examples>
Weekly conversation count trend (aggregate chart):
get_chart_data(
chart_inputs=[{"metric_id": "aggregation:session_count",
"function": "Count", "interval": "week"}],
filters=[{"field_id": "default:start_time", "operation": "after",
"value": {"scope": "relative", "value": -7776000}}])
Scatter plot of individual interview durations (values chart):
get_chart_data(
chart_inputs=[{"metric_id": "aggregation:actual_duration"}],
filters=[{"field_id": "default:start_time", "operation": "after",
"value": {"scope": "relative", "value": -7776000}}])
</examples>
Group Conversations
<usecase>
Group conversations by a field and compute metrics.
Groups conversations by a field (e.g., department, interviewer) and returns
counts and metric values per group. Prefer this tool when the user asks for
breakdowns, distributions, rankings, or "by X" questions.
POWERFUL PATTERN — AI field + grouping for large-scale analysis:
When the user wants to find trends or patterns across many conversations,
use this workflow:
1. Create an AI field (create_ai_field) that extracts the data point of interest.
2. Group by that AI field here to see the distribution of values.
Or group by another field (e.g., interviewer) and use the AI field as a metric
to see averages/counts per group.
This avoids reading raw transcripts and works at any scale.
Use metrics to compute metrics per group (e.g., total questions,
average duration). Use list_fields to discover available metrics.
</usecase>
<instructions>
See search_conversations for the complete filter format reference. IMPORTANT: Date filter values must use a dict with scope and value, not a bare date string. Example: {"field_id": "default:start_time", "operation": "after", "value": {"scope": "relative", "value": -2592000}}
Args:
group_by: Field ID to group by (e.g., "OSPT:<uuid>",
"default:interviewer"). Use list_fields to
find attributes where can_be_grouped_by is true.
report_id: ID of a saved report.
filters: Array of filter objects, each with field_id (str), operation (str), and value.
only_show_recorded_conversations: When true (default), only return recorded conversations. Set to false to also include unrecorded conversations (scheduled calls that were not recorded, or upcoming conversations that haven't happened yet).
search_term: Filter groups by name substring. Use this when
looking for a specific group instead of fetching all groups.
min_conversation_count: Minimum conversations per group to include (default 0).
offset: Pagination offset.
limit: Groups per page (default 20).
metrics: Array of metrics to compute per group. Each dict has:
metric_id (str, required) — metric ID from
list_fields (e.g., "aggregation:question_count").
function (str, required) — one of: mean, median, sum, max, min.
metric_arguments (dict, optional) — extra arguments. Check
required_arguments from list_fields
to see if this metric needs specific arguments.
For contact-scoped metrics (question_count, talk_time, minutes_late) you MUST pass {{"scope": "internal"}} or {{"scope": "external"}}. "internal" = interviewers/hiring team, "external" = non-team participants (candidates, clients, etc.).
sort_metric: How to sort groups. A dict with:
metric_id (str, required) — metric to sort by.
function (str, required) — one of: mean, median, sum, max, min.
metric_arguments (dict, optional) — same rules as above.
If omitted, groups are sorted alphabetically by label.
sort_ascending: Sort ascending (default true).
</instructions>
<response_format>
{"groups": [
{"label": "Engineering", "value": "Engineering", "conversation_count": 42,
"metric_values": {"aggregation:question_count:sum": 156.0}},
{"label": "Product", "value": "Product", "conversation_count": 28,
"metric_values": {"aggregation:question_count:sum": 98.0}}],
"total_groups": 5,
"total_conversation_count": 100,
"has_more": false,
"ai_processing": {"AI:<uuid>": {"pending_count": 20, "total_count": 100}}}
</response_format>
<notes>
Note: AI field values are computed asynchronously — if "ai_processing" is present in the response, poll by retrying the same call every ~30 seconds. Always apply tight filters before using AI fields. See search_conversations for the full AI field evaluation guidance.
IMPORTANT: When grouping by an AI field, the sum of conversation_count across
all groups may be LESS than total_conversation_count while AI processing is
in progress. Check ai_processing to see how many conversations are still being
analyzed. Wait for pending_count to reach 0 before treating group counts as
complete.
Similarly, filtering by an AI field excludes conversations that have not yet
been evaluated for that field. This means total_conversation_count itself may
be lower than expected until AI processing finishes.
</notes>
<examples>
Conversations by department in the last 90 days (get field ID from list_fields first):
group_conversations(
group_by="OSPT:<uuid>",
filters=[{"field_id": "default:start_time", "operation": "after",
"value": {"scope": "relative", "value": -7776000}}])
Top 10 interviewers by total question count (last 90 days):
group_conversations(
group_by="default:interviewer",
metrics=[{"metric_id": "aggregation:question_count",
"function": "sum",
"metric_arguments": {"scope": "internal"}}],
sort_metric={"metric_id": "aggregation:question_count",
"function": "sum",
"metric_arguments": {"scope": "internal"}},
sort_ascending=false,
limit=10,
filters=[{"field_id": "default:start_time", "operation": "after",
"value": {"scope": "relative", "value": -7776000}}])
Average interview duration by department (get field ID from list_fields first):
group_conversations(
group_by="OSPT:<uuid>",
metrics=[{"metric_id": "aggregation:actual_duration",
"function": "mean"}],
sort_metric={"metric_id": "aggregation:actual_duration",
"function": "mean"},
sort_ascending=false)
</examples>
Search Conversations
<usecase>
Search conversations with filters and get tabular data.
Returns individual conversations matching the given filters, with configurable
fields showing attribute values for each conversation.
IMPORTANT - choose the right tool AND approach based on scale:
- For "how many" questions, use group_conversations — group by the relevant attribute
and read the conversation_count per group, or use a single group to get the total. This avoids
fetching full conversation rows when only a count is needed.
- For breakdowns by attribute (e.g. by department, interviewer), use group_conversations.
- For time-series trends, use get_chart_data.
- Only use search_conversations when you need individual conversation rows.
Pick fields by scale (full strategy in the server instructions): with 1-5 matching
conversations use fields=['default:transcript']; with 5-20 use fields=['default:summary'];
with 20+ do NOT bulk-fetch transcripts or summaries — create an AI field
(create_ai_field) and aggregate it here or in group_conversations.
When unsure of scale, first query with just default fields to see total_count.
IMPORTANT - transcript token cost: A single 1-hour transcript uses roughly
10,000-15,000 tokens. Be careful not to fetch too many transcripts or else you
may use up all your context. Prefer AI fields over raw transcripts when possible.
Transcript format is one line per monologue:
Speaker Name (Role) @M:SS-M:SS: What they said...
The @start-end timestamps show when each monologue occurs (minutes:seconds).
To deep-link to a specific moment, append ?t=<seconds> to the conversation URL
(e.g. @1:23 means t=83).
IMPORTANT - summary generation: When the 'default:summary' field is included,
summaries are generated on the fly for conversations that don't have one yet.
Summary generation is SLOW (each summary analyzes the full transcript) and
produces roughly 2,000 tokens per hour of conversation. A maximum of
100 summaries can be triggered per request. To avoid long
waits, always narrow your query with tight filters (date range,
department, interviewer, etc.) BEFORE requesting summaries.
When both report_id and filters are provided, filters are applied on top
of the report's saved filters.
Each filter object has three keys: field_id (str), operation (str),
and value (format depends on attribute type, see below).
Use list_fields to discover available fields and their operations.
Use list_field_values to find valid values for a specific field.
</usecase>
<instructions>
Args:
report_id: ID of a saved report to use as the base filter set.
filters: Array of filter objects, each with field_id (str),
operation (str), and value (format depends on attribute type,
see "Filter value formats" below).
fields: Field IDs to include. If omitted, uses the report's configured
fields or defaults.
only_show_recorded_conversations: When true (default), only return recorded conversations. Set to false to also include unrecorded conversations (scheduled calls that were not recorded, or upcoming conversations that haven't happened yet).
sort_by: Field ID to sort by (default: default:start_time).
sort_ascending: Sort ascending (default: false — newest first when sorting by date).
offset: Pagination offset (default 0).
limit: Number of conversations per page (default 20, max 50).
Keep low when requesting 'default:transcript' (see transcript token cost above).
conversation_ids: Fetch specific conversations by ID (max 100). When provided,
returns these conversations directly instead of using report/filter-based
querying.
Filter value formats by attribute type:
### DATE (DateAttribute)
Operations: before, after, between
values MUST be a dict with "scope" and "value" keys. Do NOT pass a bare date string.
Absolute (single): {"scope": "absolute", "value": "2024-01-01T00:00:00.000Z"}
Absolute (range, for between): {"scope": "absolute", "value": ["2024-01-01T00:00:00.000Z", "2024-06-01T00:00:00.000Z"]}
Relative (seconds from now; negative = past): {"scope": "relative", "value": -2592000}
Common relative values: -86400 (1 day), -604800 (7 days), -2592000 (30 days), -7776000 (90 days), -15552000 (180 days), -31536000 (1 year).
### STRING (StringAttribute)
Operations: is_one_of, is_none_of, like_one_of, not_like_any_of
values: list of strings. Example: ["Engineering", "Product"]
### PERSON (PersonAttribute)
Operations: is_one_of, is_none_of
values: list of person IDs (integers). Example: [123, 456]
### NUMBER (NumberAttribute)
Operations: is, less_than, greater_than
values: a number. Example: 30.0
### BOOLEAN (BooleanAttribute)
Operations: is
values: true or false.
### CONTENT (ContentAttribute)
Operations: references, does_not_reference, references_one_of, does_not_reference_any_of
values: dict with "scope" and "value" keys.
Scope: "anyone", "internal_participant", or "external_participant".
Value: a string or list of strings.
Example: {"scope": "anyone", "value": ["Python", "TypeScript"]}
### PARTICIPANT_SCOPED_NUMBER (ParticipantScopedNumberAttribute)
Operations: is, less_than, greater_than
values: dict with "scope" and "value" keys.
Scope: "internal" or "external". Value: a number.
Example: {"scope": "external", "value": 50}
### PARTICIPANT (ParticipantAttribute)
Operations: is_one_of, is_none_of
values: list of participant IDs (UUIDs), or ["$self"] for the current user. Example: ["550e8400-e29b-41d4-a716-446655440000"]
### STRING_LIST (StringListAttribute)
Operations: includes_one_of, excludes_all_of
values: list of strings. Example: ["Engineering", "Product"]
### PERSON_LIST (PersonListAttribute)
Operations: includes_one_of, excludes_all_of
values: list of person IDs (integers). Example: [123, 456]
### PARTICIPANT_LIST (ParticipantListAttribute)
Operations: includes_one_of, excludes_all_of
values: list of participant IDs (UUIDs), or ["$self"] for the current user. Example: ["550e8400-e29b-41d4-a716-446655440000"]
### JSON (JsonAttribute)
Operations: is_one_of, is_none_of, like_one_of, not_like_any_of
values: list of strings. Example: ["value1", "value2"]
Common filter examples:
Last 30 days:
[{"field_id": "default:start_time", "operation": "after",
"value": {"scope": "relative", "value": -2592000}}]
January 2024:
[{"field_id": "default:start_time", "operation": "between",
"value": {"scope": "absolute",
"value": ["2024-01-01T00:00:00.000Z", "2024-02-01T00:00:00.000Z"]}}]
Engineering department, last 90 days (get the field ID from list_fields):
[{"field_id": "default:start_time", "operation": "after",
"value": {"scope": "relative", "value": -7776000}},
{"field_id": "OSPT:<uuid>", "operation": "is_one_of",
"value": ["Engineering"]}]
My conversations (use "$self" — resolved to your participant ID automatically):
[{"field_id": "default:contact", "operation": "includes_one_of",
"value": ["$self"]}]
My interviews as interviewer:
[{"field_id": "default:interviewer", "operation": "includes_one_of",
"value": ["$self"]}]
Specific candidate (use contact UUID from list_field_values):
[{"field_id": "default:candidate", "operation": "includes_one_of",
"value": ["<candidate-contact-uuid>"]}]
Specific interviewer (use contact UUID from list_field_values):
[{"field_id": "default:interviewer", "operation": "includes_one_of",
"value": ["<interviewer-contact-uuid>"]}]
</instructions>
<response_format>
{"field_definitions": [{"field_id": "default:start_time", "name": "Start Time", "type": "date"}],
"conversations": [{"id": 12345,
"url": "https://<app-domain>/notes/12345",
"fields": {"default:start_time": [{"value": "2024-01-15", "label": "Jan 15"}],
"default:participant_v2": [{"value": "<uuid>", "label": "Jane Doe",
"details": {"name": "Jane Doe", "emails": ["jane@acme.com"],
"phone_numbers": ["+15551234567"]}}]}}],
"total_count": 142,
"has_more": true,
"ai_processing": {"AI:<uuid>": {"pending_count": 20, "total_count": 100}}}
</response_format>
<notes>
Note: AI field values are processed asynchronously. If "ai_processing" is present in the response, some values are still being computed. Poll by retrying the same call every ~30 seconds until the data is complete.
IMPORTANT: Including AI fields triggers analysis for every matching conversation that hasn't been analyzed yet. Analysis runs AI evaluation and persists the results to the workspace, where they are visible to other users in reports. Always apply tight filters (date range, department, interviewer, etc.) to keep the set small and get results quickly. Broad unfiltered queries will be slow and may hit evaluation limits that require narrowing your search.
Entity-valued fields (participant or person types) include a 'details' object per value with the entity's name and all known emails and phone numbers, for matching to your own records.
</notes>
<examples>
Engineering conversations in the last 30 days with department and interviewer fields:
First, find the department field ID: list_fields(search_term="department")
Then use the returned OSPT:<uuid> field ID:
search_conversations(
filters=[
{"field_id": "default:start_time", "operation": "after",
"value": {"scope": "relative", "value": -2592000}},
{"field_id": "OSPT:<uuid>", "operation": "is_one_of",
"value": ["Engineering"]}],
fields=["default:start_time", "default:calendar_event_title", "OSPT:<uuid>"])
</examples>
<access_scope>Results are scoped server-side to interviews and candidates the authenticated user can already access in Metaview, within their workspace and role. Intended for the user's normal hiring workflow (reviewing, summarizing, exporting, ATS sync). Read-only.</access_scope>
Search Reports
<usecase>
List saved reports the user has access to, or fetch full details for specific reports.
A report is a saved configuration — a named set of filters, fields,
grouping, and charts that defines a particular way to analyze interview
conversations. Pass a report's ID to search_conversations, group_conversations,
or get_chart_data to reuse its saved filters.
Use report_ids to fetch one or more specific reports by ID.
Set include_detail=true to get the full configuration (filters, fields,
grouping, charts) instead of just the summary.
BEST PRACTICE: Use search_term when looking for a specific report
by name instead of listing all reports and scanning.
</usecase>
<instructions>
Args:
report_ids: List of report IDs to fetch. Returns only these reports.
include_detail: When true, returns full report configuration (filters,
fields, grouping, charts) for each report. Default false (summary only).
search_term: Filter reports by name (case-insensitive). Supports substring
matching and regex patterns (automatically detected).
category: Filter by category. One of: createdByMe, createdByMetaview,
recentlyViewed, favorited.
sort_key: Sort field. One of: updated_at (default), name, created_at.
sort_order: Sort direction. One of: desc (default), asc.
offset: Pagination offset (default 0).
limit: Number of results per page (default 20, max 100).
</instructions>
<response_format>
{"reports": [
{"id": "abc-123", "name": "Engineering Interviews Q1",
"url": "https://<app-domain>/reports/abc-123",
"description": "All engineering interviews in Q1 2024",
"is_default": false,
"created_by": {"name": "Alice Smith"},
"created_at": "2024-01-01 00:00:00+00:00",
"updated_at": "2024-03-15 10:30:00+00:00"}],
"count": 1, "has_more": false}
</response_format>
<examples>
search_reports()
search_reports(search_term="engineering")
search_reports(category="createdByMe", sort_key="updated_at")
search_reports(report_ids=["abc-123"], include_detail=true)
search_reports(report_ids=["abc-123", "def-456"])
</examples>
Create Report
<usecase>
Create a new report or update an existing one in the Metaview web-app.
IMPORTANT: Only use this tool when the user explicitly asks to create or edit
a saved report in Metaview. Do NOT use this tool for ad-hoc data analysis, answering
questions, or running queries — use search_conversations, group_conversations,
or get_chart_data for those. This tool persists a report that appears in
the user's Reports list in the web-app.
Before calling this tool, confirm with the user what they want the report to contain
(name, filters, grouping, fields). Do not create reports speculatively.
After creating or updating a report, share the `url` from the response with the user
so they can open it in the web-app.
</usecase>
<instructions>
Creating: Omit report_id. You MUST provide filters (can be an empty list for "all
conversations"). Optionally set name, fields, grouping, and other configuration in the
same call.
Updating: Provide report_id of an existing report. Only the fields you include will
be changed — omitted fields are left unchanged.
Use list_fields to find valid field IDs for filters, fields,
and grouping. Use list_field_values to find valid filter values.
Args:
report_id: ID of an existing report to update. Omit to create a new report.
filters: Array of filter objects (required for create, optional for update).
See search_conversations for the complete filter format reference. IMPORTANT: Date filter values must use a dict with scope and value, not a bare date string. Example: {"field_id": "default:start_time", "operation": "after", "value": {"scope": "relative", "value": -2592000}}
name: Human-readable report name (e.g., "Engineering Interviews Q1").
description: Report description.
group: Field ID to group conversations by (e.g., "OSPT:<uuid>").
Use list_fields to find field IDs.
remove_grouping: Set to true to remove grouping from a report. Required because
passing group=null is indistinguishable from omitting the parameter.
field_ids: List of field IDs to show as columns
(e.g., ["default:start_time", "OSPT:<uuid>"]).
sort_field: Field ID to sort by. Must be one of the field_ids.
sort_ascending: Sort ascending (true) or descending (false). Default true.
metrics: Metrics for grouped reports. Array of objects, each with:
metric_id (str), function (str: mean/median/max/min/sum),
footer_aggregation_function (str), metric_arguments (dict).
sort_metric: Which metric to sort groups by. Object with:
metric_id (str), function (str),
metric_arguments (dict). Pass null to sort by the group label.
sort_metric_ascending: Sort groups ascending or descending. Default true.
only_show_recorded_conversations: Include only recorded conversations. Default true.
expand_rows: Expand table rows. Default false.
min_group_conversation_count: Hide groups with fewer conversations than this.
</instructions>
<examples>
Common filter examples (date ranges, departments, "$self", specific people)
are documented on search_conversations.
Example — create a report:
create_report(
name="Engineering Last 30 Days",
filters=[{"field_id": "default:start_time", "operation": "after",
"value": {"scope": "relative", "value": -2592000}},
{"field_id": "OSPT:<uuid>", "operation": "is_one_of",
"value": ["Engineering"]}],
field_ids=["default:start_time", "default:interviewer",
"default:candidate"],
sort_field="default:start_time",
sort_ascending=false
)
Example — rename a report:
create_report(report_id="abc-123", name="New Report Name")
</examples>
Get Enrichment Status
<usecase>
Get the workspace's enrichment credit status, usage breakdown, and optionally
list individual enrichment attempts.
Always returns:
- Monthly credit allowance and remaining balance
- Usage breakdown by enrichment type (email vs phone) for the requested month
- Active top-up credit purchases with remaining balances and expiry dates
Optionally returns:
- Per-user usage breakdown (include_usage_by_user=true)
- Individual enrichment attempts (include_enrichment_attempts=true)
</usecase>
<instructions>
Args:
person_id: Filter usage and attempts to a specific person (admin only).
Non-admins always see only their own data regardless of this parameter.
month: Month to report on as YYYY-MM (e.g. "2026-01"). When provided,
usage is scoped to that calendar month. When omitted, usage covers
the current month to date (no end boundary).
include_usage_by_user: When true, include per-user usage breakdown.
Default false.
include_enrichment_attempts: When true, append the list of individual enrichment
attempts. Default false.
enrichment_type: Filter attempts by type: "email" or "phone".
Only used when include_enrichment_attempts=true.
offset: Pagination offset for attempts (default 0).
limit: Number of attempts per page (default 50, max 100).
</instructions>
<response_format>
{"credits": {
"monthly_limit": 500,
"monthly_used": 450,
"monthly_remaining": 50,
"top_up_credits_available": 200,
"total_available": 250,
"warning_threshold_reached": false},
"usage": {
"email": {"enrichments_count": 50, "credits_used": 150},
"phone": {"enrichments_count": 30, "credits_used": 300},
"total_credits_used": 450},
"usage_by_user": [{...}],
"top_ups": [{...}],
"enrichment_attempts": [{...}],
"enrichment_attempts_total_count": 150,
"enrichment_attempts_has_more": true}
</response_format>
<notes>
credits reflects the current live balance regardless of the month parameter.
usage is scoped to the requested month.
All workspaces receive a base allowance of 500 credits per month.
usage_by_user is only present when include_usage_by_user=true.
enrichment_attempts fields are only present when include_enrichment_attempts=true.
</notes>
<examples>
Check credit status and usage:
get_enrichment_status()
Usage for January 2026:
get_enrichment_status(month="2026-01")
Per-user breakdown:
get_enrichment_status(include_usage_by_user=true)
Include individual enrichment attempts:
get_enrichment_status(include_enrichment_attempts=true)
</examples>
Find Candidate In Sequences
<usecase>
Check if a candidate is enrolled in any sequences.
Look up a candidate by ID, LinkedIn URL, email address, or phone number and
return all sequences they are (or were) enrolled in, including sequences created
by other users. Useful before adding someone to a new sequence to avoid duplicate
outreach.
Provide at least one lookup parameter. If candidate_id is given, it is used
directly. Otherwise, linkedin_url/email/phone_number are matched with OR logic.
</usecase>
<instructions>
Args:
candidate_id: Candidate ID for direct lookup (preferred when known).
linkedin_url: LinkedIn profile URL (normalized automatically).
email: Email address to match against candidate primary email or
the to_email used in the sequence.
phone_number: Phone number to match against candidate primary phone.
</instructions>
<response_format>
{"candidates": [
{"candidate_id": "...", "candidate_name": "John Doe",
"candidate_email": "john@example.com", "candidate_phone": "+1234567890",
"candidate_linkedin_url": "https://www.linkedin.com/in/johndoe",
"sequences": [
{"sequence_id": "...", "sequence_name": "Outreach v2",
"status": "active",
"created_at": "2024-01-10T...",
"steps_sent": 2, "steps_total": 4}]}]}
</response_format>
<examples>
Check by LinkedIn:
find_candidate_in_sequences(linkedin_url="https://linkedin.com/in/johndoe")
Check by email:
find_candidate_in_sequences(email="john@example.com")
Check by multiple identifiers:
find_candidate_in_sequences(linkedin_url="...", email="john@example.com")
</examples>
List Mailboxes
<usecase>
List the email mailboxes you can send sequence emails from.
Returns your own active mailboxes plus any mailboxes where you have
send-on-behalf-of permission. Use the mailbox id as from_mailbox_id
when creating EMAIL steps in a sequence.
</usecase>
<instructions>No parameters needed.</instructions>
<response_format>
{"mailboxes": [
{"id": "abc-123", "email_address": "alice@acme.com",
"person_id": 123, "person_name": "Alice Smith",
"is_default": true, "is_owned_by_me": true}]}
</response_format>
Manage Candidate Sequence
<usecase>
Add candidates to a sequence, or pause, resume, cancel, remove, or update a candidate's enrollment.
Dispatches based on the action parameter. Only the sequence creator can perform these actions.
IMPORTANT: Always confirm with the user before calling this tool. Describe the exact changes you plan to make and get explicit approval first.
</usecase>
<instructions>
Args:
action: Required. One of: add, pause, resume, cancel, remove, update.
- cancel: Stop sending further emails but keep the candidate visible in the sequence.
- remove: Delete the candidate from the sequence entirely (archive/soft-delete).
sequence_id: Required for add.
candidate_ids: Required for add. List of candidate IDs to enroll.
candidate_sequence_id: Required for pause, resume, cancel, remove.
candidate_sequence_step_id: Required for update. Get this from
list_sequence_candidates(sequence_id=..., include_detail=true).
subject_override: Optional for update. Custom subject for this candidate's step.
Supports {{variable_name}} placeholders filled in per-candidate by AI before sending.
template_override: Optional for update. Custom body for this candidate's step.
Supports {{variable_name}} placeholders filled in per-candidate by AI before sending.
clear_subject_override: Optional for update. Set true to revert subject to template default.
clear_template_override: Optional for update. Set true to revert body to template default.
schedule_override: Optional for update. JSON object to override the step's schedule.
Supported types: {"schedule_type": "instant"}, {"schedule_type": "next_business_day", "time_of_day": "HH:MM"}, {"schedule_type": "later", "delay_seconds": N}.
Setting a specific time is NOT supported.
clear_schedule_override: Optional for update. Set true to revert schedule to template default.
context_source_type: Optional for add. Must be one of 'search' or 'project'.
If provided, context_source_id is also required.
IMPORTANT: Leave both context_source_type and context_source_id null
if the source is unknown — do NOT guess or fabricate a value.
context_source_id: Optional for add. UUID of the search or project the candidates came from.
If provided, context_source_type is also required.
Leave null if context_source_type is null.
</instructions>
<response_format>
Add: {"added": N, "sequence_id": "...", "candidate_sequences": [...]}
Pause/resume/cancel/remove: {"action": "paused|resumed|cancelled|removed", "candidate_sequence_id": "..."}
Update: {"updated": true, "candidate_sequence_step_id": "..."}
</response_format>
<examples>
Add candidates:
manage_candidate_sequence(action="add", sequence_id="abc", candidate_ids=["c1", "c2"], context_source_type="search", context_source_id="search-uuid")
Pause:
manage_candidate_sequence(action="pause", candidate_sequence_id="xyz")
Resume:
manage_candidate_sequence(action="resume", candidate_sequence_id="xyz")
Cancel:
manage_candidate_sequence(action="cancel", candidate_sequence_id="xyz")
Remove (archive):
manage_candidate_sequence(action="remove", candidate_sequence_id="xyz")
Override step template for a candidate:
manage_candidate_sequence(action="update", candidate_sequence_step_id="step1", template_override="Custom body")
Override step schedule for a candidate:
manage_candidate_sequence(action="update", candidate_sequence_step_id="step1", schedule_override={"schedule_type": "later", "delay_seconds": 86400})
</examples>
Manage Sequence
<usecase>
Create, update, duplicate, or delete a sequence.
Dispatches based on the action parameter. Only the creator of a sequence can update or delete it; admins and creators can duplicate.
Test sends, email previews, and send-now are NOT available via this tool. Direct users to the Metaview web app for these.
IMPORTANT: Always confirm with the user before calling this tool. Describe the exact changes you plan to make and get explicit approval first.
</usecase>
<instructions>
Args:
action: Required. One of: create, update, duplicate, delete.
sequence_id: Required for update, duplicate, delete.
name: Required for create, optional for update.
description: Optional for create and update.
set_enabled: Optional for create and update. Enable or disable the sequence.
steps: Optional for update. A JSON list of step objects to replace the current steps.
Steps not in the list are deleted. Order in the list determines step order.
Each step object has:
channel_type: str (REQUIRED — one of: EMAIL, LINKEDIN_CONNECTION, LINKEDIN_INMAIL, LINKEDIN_MESSAGE, SMS, PHONE, WHATSAPP)
sequence_step_id: str | null (include the existing step ID to update it;
omit or set null for new steps — an ID will be generated automatically)
template: str | null (message body; supports {{variable_name}} placeholders
that are filled in per-candidate by an AI call before sending)
description: str | null
schedule: dict | null (e.g. {"schedule_type": "instant"}, {"schedule_type": "later", "delay_seconds": 86400}, {"schedule_type": "next_business_day", "time_of_day": "09:00:00"}.
Setting a specific time is NOT supported.
step_execution_type: str | null (AUTOMATIC or MANUAL)
execution_timeout_seconds: int | null (only for MANUAL steps)
EMAIL step fields:
from_mailbox_id: REQUIRED (mailbox UUID — use list_mailboxes to get available mailboxes)
step_type: REQUIRED (NEW_THREAD or REPLY)
bcc: optional (list of email addresses)
cc: optional (list of email addresses)
step_execution_type: AUTOMATIC, MANUAL (default: AUTOMATIC)
NOT used: from_linkedin_account_id, from_whatsapp_account_id, linkedin_subject
step_type values: NEW_THREAD or REPLY
subject: str | null (REQUIRED for NEW_THREAD; forbidden for REPLY. Supports {{variable_name}} placeholders)
LINKEDIN_CONNECTION step fields:
from_linkedin_account_id: optional (LinkedInAccount UUID — the sender's connected LinkedIn account)
step_execution_type: AUTOMATIC, MANUAL (default: AUTOMATIC)
NOT used: bcc, cc, from_mailbox_id, from_whatsapp_account_id, linkedin_subject, step_type
Connection request with optional 300-char note (stored in template field)
LINKEDIN_INMAIL step fields:
linkedin_subject: REQUIRED (str — required subject line for the InMail)
from_linkedin_account_id: optional (LinkedInAccount UUID — the sender's connected LinkedIn account)
step_execution_type: AUTOMATIC, MANUAL (default: AUTOMATIC)
NOT used: bcc, cc, from_mailbox_id, from_whatsapp_account_id, step_type
InMail to non-connections. Requires subject + body. Uses Premium LinkedIn credits.
LINKEDIN_MESSAGE step fields:
from_linkedin_account_id: optional (LinkedInAccount UUID — the sender's connected LinkedIn account)
linkedin_subject: optional (str | null — subject line for the LinkedIn message)
step_execution_type: AUTOMATIC, MANUAL (default: AUTOMATIC)
NOT used: bcc, cc, from_mailbox_id, from_whatsapp_account_id, step_type
subject shorthand: setting 'subject' auto-maps to linkedin_subject
SMS step fields:
step_execution_type: MANUAL (default: MANUAL)
NOT used: bcc, cc, from_linkedin_account_id, from_mailbox_id, from_whatsapp_account_id, linkedin_subject, step_type
PHONE step fields:
step_execution_type: MANUAL (default: MANUAL)
NOT used: bcc, cc, from_linkedin_account_id, from_mailbox_id, from_whatsapp_account_id, linkedin_subject, step_type
WHATSAPP step fields:
from_whatsapp_account_id: REQUIRED (WhatsappAccount UUID — the sender's connected WhatsApp account)
step_execution_type: AUTOMATIC, MANUAL (default: AUTOMATIC)
NOT used: bcc, cc, from_linkedin_account_id, from_mailbox_id, linkedin_subject, step_type
</instructions>
<response_format>
Create/update/duplicate: {"sequence": {sequence detail object with steps}}
Delete: {"deleted": true, "sequence_id": "..."}
</response_format>
<examples>
Create a sequence:
manage_sequence(action="create", name="Outreach v2")
Update name and enable:
manage_sequence(action="update", sequence_id="abc", name="New Name", set_enabled=true)
Duplicate:
manage_sequence(action="duplicate", sequence_id="abc")
Delete:
manage_sequence(action="delete", sequence_id="abc")
</examples>
List Sequence Candidates
<usecase>
List candidates enrolled in a sequence, or get a specific candidate's full journey.
Non-admins can only access sequences they created.
By default returns a summary per candidate. Use include_detail for per-step
delivery status, and include_messages to see the actual email content.
Pass candidate_sequence_id to jump straight to a specific candidate's full detail.
</usecase>
<instructions>
Args:
sequence_id: UUID of the sequence (required unless candidate_sequence_id is provided).
candidate_sequence_id: Fetch a specific candidate's full journey directly.
When provided, include_detail and include_messages are implied.
include_detail: When true, include per-step delivery status (sent, delivered,
opened, replied, bounced, skipped) and candidate_sequence_step_id for each
candidate. Required to get step IDs for updates via manage_candidate_sequence.
Default false.
include_messages: When true, include email content for all sent steps.
Requires include_detail. Default false.
status: Filter by status: "active", "paused", "cancelled", or "completed".
Omit to see all statuses.
search_term: Filter by candidate name (case-insensitive substring match).
offset: Pagination offset (default 0).
limit: Number of results per page (default 50, max 100).
</instructions>
<response_format>
{"sequence_id": "...", "sequence_name": "Outreach v2",
"candidates": [
{"candidate_sequence_id": "...",
"candidate_id": "...", "candidate_name": "John Doe",
"candidate_email": "john@example.com", "candidate_phone": "+1234567890",
"to_email": "john@example.com",
"status": "active",
"paused_reason": null, "cancelled_reason": null,
"outcome": null,
"created_by": {"name": "Alice Smith"},
"created_at": "2024-01-10T...",
"steps_sent": 2, "steps_total": 4,
"last_sent_at": "2024-01-15T...",
"has_reply": true,
"steps": [{"candidate_sequence_step_id": "...", "step_id": "...", ...}],
"messages": [{...}]}],
"total_count": 42,
"has_more": false}
</response_format>
<examples>
List all candidates in a sequence:
list_sequence_candidates(sequence_id="abc-123")
Active candidates with delivery detail:
list_sequence_candidates(sequence_id="abc-123", status="active", include_detail=true)
Full journey for a specific candidate (with emails):
list_sequence_candidates(candidate_sequence_id="xyz-789")
</examples>
List Sequences
<usecase>
List sequences the current user has access to, or get full details for a specific sequence.
Admins can see all sequences in the workspace. Non-admins can see sequences
they created plus any that send from their mailbox. Archived sequences are always excluded.
Returns summary data including per-sequence stats so the caller can compare
sequences without further round-trips. Pass sequence_id or include_detail=true
for full step configuration.
</usecase>
<instructions>
Args:
sequence_id: Fetch a specific sequence with full detail (steps, templates, etc.).
When provided, include_detail is implied.
created_by_person_id: Only return sequences created by this person.
Useful for admins who want to see just their own sequences.
search_term: Filter sequences whose name contains this substring
(case-insensitive). Also supports regex when the term contains
special characters like .*+?[]|().
include_detail: When true, include full step configuration for each sequence.
Default false (summary only).
include_disabled: Whether to include disabled sequences (default true).
offset: Pagination offset (default 0).
limit: Number of sequences per page (default 20, max 50).
</instructions>
<response_format>
{"sequences": [
{"id": "...", "name": "Outreach v2", "description": "...",
"is_enabled": true, "is_valid": true, "is_modifiable": false,
"created_by": {"name": "Alice Smith"},
"created_at": "2024-01-01T...", "updated_at": "2024-01-10T...",
"step_count": 3,
"stats": {"total_candidates": 42, "total_active": 10,
"total_completed": 20, "total_paused": 2,
"total_sent": 100, "total_replies": 15,
"total_bounced": 3, "total_interested": 8},
"steps": [{...}]}],
"total_count": 5,
"has_more": false}
</response_format>
<examples>
All enabled sequences:
list_sequences(include_disabled=false)
Search by name:
list_sequences(search_term="outreach")
Full detail for a specific sequence:
list_sequences(sequence_id="abc-123")
Only sequences created by a specific person:
list_sequences(created_by_person_id=123)
</examples>
<data_model>
- A SEQUENCE is an outreach campaign with one or more steps. Sequences can be enabled/disabled, and become locked (non-modifiable) once the first email has been sent. Only the creator can edit a sequence. Sending test emails and previewing emails should be done via the Metaview web app.
- A STEP is a single action in a sequence. Supported channel types: EMAIL, LINKEDIN_CONNECTION, LINKEDIN_INMAIL, LINKEDIN_MESSAGE, SMS, PHONE, WHATSAPP. Each step has content templates and a schedule (when to execute). Steps can execute automatically or manually (with a timeout). See per-channel step fields for which execution types each channel supports.
- A CANDIDATE SEQUENCE is the enrollment of a candidate into a sequence. It tracks delivery status per step and can be paused, resumed, or cancelled. Individual step email templates and schedules can be customized per candidate via overrides.
- A MAILBOX represents an email account that can send sequence emails. list_mailboxes returns the user's available mailboxes, each with: id (the mailbox UUID — use as from_mailbox_id when creating EMAIL steps), email_address, person_id, person_name, is_default, is_owned_by_me (false when access is via send-on-behalf-of permission).
- A SEND-ON-BEHALF PERMISSION allows one person to send sequence emails as another person. Each step has a from_mailbox_id (the sender's mailbox). To send as someone else, that person must grant you permission. Permissions can be granted to a specific person or to the entire workspace.
</data_model>
Get Sourcing Analytics
<usecase>
Get aggregate sourcing metrics for your workspace with flexible filtering and grouping.
Use this to answer questions like:
- How many searches / candidates / feedback events in a time period?
- What is the acceptance rate overall, per user, or per search?
- Who created the most searches or gave the most feedback?
- Weekly/monthly trends of sourcing activity
- How many searches are on recurring schedules or linked to sequences?
- Are we using all our sourcing seats?
</usecase>
<instructions>
PERMISSIONS:
- Non-admin users: results are always scoped to their own data (searches they created, feedback they gave). The user_id parameter is ignored for non-admins.
- Admin users: results cover the entire workspace. Admins can optionally pass user_id to filter to a specific team member.
- seat_utilization metric: admin-only.
- search_id: verified against the user's access (ownership, sharing, or admin).
Args:
metric (REQUIRED): What to measure. One of: acceptance_rate, accepted, candidates, candidates_without_feedback, feedback, maybe, rejected, scheduled_searches, searches, searches_with_sequences, seat_utilization
time_period: Shorthand time filter. One of: last_30_days, last_7_days, last_90_days, last_month, last_quarter, last_week, this_month, this_quarter, this_week, today, yesterday
start_date: ISO date (YYYY-MM-DD) for custom range start. Overrides time_period.
end_date: ISO date (YYYY-MM-DD) for custom range end.
group_by: Break down results. One of: day, feedback_value, mode, month, search, user, week
user_id: (admin only) Filter to a specific user's data by person ID.
search_id: Filter to a specific search (UUID). Must be accessible to the user.
include_archived: Include archived searches (default false).
limit: Max groups to return (default 20, max 100).
offset: Pagination offset for grouped results (default 0).
For comparisons (e.g. this month vs last month), call this tool twice with different time_period values.
</instructions>
<response_format>{"metric": "acceptance_rate", "value": 38.4, "groups": [{"label": "Alice Smith", "id": 123, "value": 52.1, "count": 245}], "total_groups": 45, "has_more": true, "time_range": {"start": "2026-04-01", "end": "2026-04-29"}, "filters_applied": {"time_period": "this_month"}}</response_format>
<examples>
Searches this month:
get_sourcing_analytics(metric="searches", time_period="this_month")
Weekly candidate trend:
get_sourcing_analytics(metric="candidates", group_by="week", time_period="last_90_days")
Acceptance rate by user:
get_sourcing_analytics(metric="acceptance_rate", group_by="user", time_period="last_30_days")
Check seat utilization:
get_sourcing_analytics(metric="seat_utilization")
</examples>
<data_model>See get_search_details for the sourcing data model definitions (searches, agent phases, candidates, feedback, ICPs, scheduled searches, search-sequence links, posting to ATS).</data_model>
Get Sourcing Messages
<usecase>
Retrieve the conversation history for a sourcing search.
Returns messages between you and the sourcing agent, including text messages and structured attachments. Structured attachments (candidates, ICP, calibration, file, filter_proposal) are rendered as separate messages in the response.
Use this to:
- Check the agent's latest response after sending a message
- Review the full conversation history
- See which candidates were surfaced (candidate attachments show IDs — use list_sourcing_candidates or fetch_candidates to get full details)
Poll this after sending a message. The agent phase indicates progress:
- busy: Agent is still working. Poll again in a few seconds.
- idle/waiting: Agent has finished and is waiting for your input.
- error: Something went wrong.
</usecase>
<instructions>
Args:
search_id: The search ID to retrieve messages for.
limit: Maximum number of messages to return (default 50, max 200).
offset: Number of messages to skip for pagination (default 0). Messages are ordered chronologically.
</instructions>
<response_format>{"search_id": "550e8400-...", "phase": "idle", "messages": [{"role": "user", "content": "Find me senior backend engineers...", "timestamp": "2025-01-15T10:30:00Z", "message_type": "text"}, {"role": "assistant", "content": "Candidates presented: id1, id2, id3", "timestamp": "2025-01-15T10:32:00Z", "message_type": "candidates"}], "total_count": 12, "has_more": false, "next": "The agent is idle. Read the latest message to understand the current state..."}</response_format>
<examples>
Get latest messages:
get_sourcing_messages(search_id="550e8400-...")
Paginate:
get_sourcing_messages(search_id="550e8400-...", limit=20, offset=20)
</examples>
<access_scope>Results are scoped server-side to interviews and candidates the authenticated user can already access in Metaview, within their workspace and role. Intended for the user's normal hiring workflow (reviewing, summarizing, exporting, ATS sync). Read-only.</access_scope>
Get Search Details
<usecase>
Get the full state of a sourcing search: the current ICP (Ideal Candidate Profile), ICP version history, calibration progress (feedback counts and acceptance rate), pack history, and the agent's current phase.
Use this to answer questions about a search directly from data instead of messaging the sourcing agent with send_sourcing_message (which takes minutes to respond). Questions this tool answers instantly:
- What is the ICP for this search? How has it evolved?
- How calibrated is this search? What's the acceptance rate?
- What feedback has been given so far?
- How many candidate packs have been presented, and when?
- Is the agent currently working, idle, or waiting for input?
Combine with list_sourcing_candidates for per-candidate detail and get_sourcing_analytics for cross-search metrics.
</usecase>
<instructions>
Args:
search_id: The search to inspect.
icp_version: Return the content of a specific ICP version instead of the latest. Omit to get the latest ICP. Use icp_versions in the response to discover which versions exist.
</instructions>
<response_format>{"search_id": "550e8400-...", "title": "Senior Backend Engineer", "mode": "source", "phase": "idle", "created_at": "2025-01-15T10:30:00Z", "updated_at": "2025-01-16T14:22:00Z", "icp": {"version": 3, "content": "# Ideal Candidate Profile...", "created_at": "2025-01-16T09:00:00Z"}, "icp_versions": [{"version": 3, "created_at": "2025-01-16T09:00:00Z"}, {"version": 2, "created_at": "2025-01-15T16:45:00Z"}], "calibration": {"total_candidates": 42, "reviewed": 30, "unreviewed": 12, "yes": 18, "no": 9, "maybe": 3, "acceptance_rate": 0.6, "recent_feedback": [{"verdict": "NO", "text": "Too junior", "candidate_id": "c1a2b3d4-...", "candidate_name": "Jane Smith", "created_at": "2025-01-16T11:00:00Z"}]}, "packs": [{"pack": 1, "candidate_count": 10, "presented_at": "2025-01-15T12:00:00Z"}]}</response_format>
<notes>
The ICP may be null for older searches that predate ICP persistence. In that case, the ICP can be found in the conversation history via get_sourcing_messages.
recent_feedback contains the latest verdict per candidate (up to 10 most recent). The calibration counts cover all candidates surfaced in the search.
</notes>
<examples>
Get the current state of a search:
get_search_details(search_id="550e8400-...")
Get an earlier ICP version to compare how the ICP evolved:
get_search_details(search_id="550e8400-...", icp_version=1)
</examples>
<data_model>
- A SEARCH is a sourcing conversation with an AI agent that finds candidates matching your requirements. Each search has a conversation thread, an ICP (Ideal Candidate Profile), and a list of surfaced candidates. Searches have a mode (SOURCE or RESEARCH) and can be active or archived.
- The agent goes through phases: idle (waiting for input), busy (actively searching), waiting (needs your feedback on candidates), error (something went wrong).
- CANDIDATES are people surfaced by the agent. Each has a profile summary and reasoning sections explaining why they match.
- FEEDBACK is a YES, NO, or MAYBE verdict given by a user on a candidate. The acceptance rate is the percentage of YES out of total feedback.
- An ICP is the Ideal Candidate Profile the agent builds from your conversation. It evolves as you provide feedback and refine requirements.
- A SCHEDULED SEARCH is a search configured to run on a recurring schedule.
- A SEARCH-SEQUENCE LINK connects a sourcing search to an outreach sequence, so accepted candidates flow into automated outreach.
- POSTING TO ATS exports candidates from a search into the workspace's connected ATS (Applicant Tracking System, e.g. Greenhouse, Lever, Ashby): the candidate is created in the ATS with an application on a chosen job and stage, and the sourcing agent's reasoning is added as an ATS note.
</data_model>
Give Sourcing Feedback
<usecase>
Submit feedback on one or more candidates in a sourcing search.
Feedback calibrates the sourcing agent — accepting or rejecting candidates helps it refine its search and find better matches.
Supports bulk feedback: submit feedback for multiple candidates in a single call. Maximum 50 candidates per call.
After submitting feedback, send a message using send_sourcing_message (e.g. 'Please search for more candidates based on my feedback') to trigger the agent to refine its search. The agent does NOT automatically restart.
</usecase>
<instructions>
Args:
search_id: The search containing the candidates.
feedbacks: Array of feedback objects. Each must have:
- candidate_id (str): The candidate ID.
- accepted (str): One of YES, MAYBE, or NO.
- text (str, optional): Free-text feedback explaining your decision.
Feedback values:
YES: Strong match, move forward.
MAYBE: Possible match, keep in consideration.
NO: Not a match, do not consider further.
</instructions>
<response_format>{"search_id": "550e8400-...", "results": [{"candidate_id": "c1a2b3d4-...", "success": true}, {"candidate_id": "invalid-id", "success": false, "error": "Candidate not found in this search"}], "next": "Feedback recorded. The sourcing agent does NOT automatically refine..."}</response_format>
<examples>
Accept a single candidate:
give_sourcing_feedback(search_id="550e8400-...", feedbacks=[{"candidate_id": "c1a2b3d4", "accepted": "YES", "text": "Great Python experience"}])
Bulk feedback:
give_sourcing_feedback(search_id="550e8400-...", feedbacks=[{"candidate_id": "c1a2b3d4", "accepted": "YES"}, {"candidate_id": "d4e5f6a7", "accepted": "NO", "text": "Missing distributed systems experience"}])
</examples>
List Sourcing Candidates
<usecase>
List candidates surfaced in a sourcing search with profile summaries and feedback status.
Returns candidates ordered by when they were surfaced (newest first), with:
- Profile summary sections (experience, skills, education, etc.)
- Current feedback status (if any)
- Pack number indicating which batch the candidate was presented in
- Remaining candidate credits on the user's plan
Hidden candidates (those not yet revealed due to credit limits) are excluded.
IMPORTANT for large result sets: For searches with 100+ candidates, paginate with limit=50 and iterate through pages using offset. Do NOT attempt to fetch all candidates in a single call — this will exceed context limits for most AI agents.
Use this to:
- Review candidates the agent has found
- Check which candidates still need feedback
- Filter by pack to see only the latest batch of candidates
- Get candidate IDs for use with give_sourcing_feedback
- Get candidate IDs referenced in get_sourcing_messages candidate attachments
</usecase>
<instructions>
Args:
search_id: The search to list candidates for.
pack: Filter to a specific pack (batch, 1-based). Omit to list all candidates.
limit: Maximum candidates to return (default 20, max 100).
offset: Pagination offset (default 0).
detail_level: Controls how much data is returned per candidate.
'minimal' — candidate_id, name, linkedin_url only. Smallest response, best for getting an overview or building ID lists.
'summary' — (default) includes reasoning summary sections from the sourcing agent (experience, skills, education highlights).
'full' — includes summary PLUS the candidate's full profile data (location, experience history, education, skills list). Larger response but avoids needing separate fetch_candidates calls.
feedback_status: Filter candidates by their feedback status.
'all' — (default) return all candidates.
'unreviewed' — only candidates with no feedback yet.
'yes' — only candidates marked YES.
'no' — only candidates marked NO.
'maybe' — only candidates marked MAYBE.
</instructions>
<response_format>{"search_id": "550e8400-...", "candidates": [{"candidate_id": "c1a2b3d4-...", "name": "Jane Smith", "linkedin_url": "https://linkedin.com/in/jane-smith", "pack": 2, "summary": [{"title": "Experience", "description": "10 years in backend..."}], "feedback": {"accepted": "YES", "text": "Great experience", "author": "Alice"}}], "total_count": 12, "total_packs": 3, "has_more": false, "remaining_credits": 88, "next": "Review these candidates and provide feedback..."}</response_format>
<examples>
List all candidates (default summary detail):
list_sourcing_candidates(search_id="550e8400-...")
List only the latest pack:
list_sourcing_candidates(search_id="550e8400-...", pack=3)
Get minimal list for overview:
list_sourcing_candidates(search_id="550e8400-...", detail_level="minimal", limit=100)
Get only unreviewed candidates with full profiles:
list_sourcing_candidates(search_id="550e8400-...", detail_level="full", feedback_status="unreviewed", limit=50)
Paginate through results:
list_sourcing_candidates(search_id="550e8400-...", limit=50, offset=50)
</examples>
List Sourcing Searches
<usecase>
List your sourcing and research searches with summary information.
Returns summary detail per search (title, mode, candidate count, agent phase, timestamps). For a single search's full state — ICP, ICP version history, calibration progress, and pack history — use get_search_details.
Use this to:
- See all your active searches
- Find a specific search to resume
- Check which searches have candidates waiting for feedback
- Distinguish between source and research searches via the mode field
</usecase>
<instructions>
Args:
limit: Maximum number of searches to return (default 20, max 50).
offset: Pagination offset (default 0).
</instructions>
<response_format>{"searches": [{"id": "550e8400-...", "title": "Senior Backend Engineer", "mode": "source", "phase": "waiting", "candidate_count": 12, "created_at": "2025-01-15T10:30:00Z", "updated_at": "2025-01-16T14:22:00Z"}], "total_count": 8, "has_more": false}</response_format>
<examples>
List all searches:
list_sourcing_searches()
Paginate:
list_sourcing_searches(limit=10, offset=10)
</examples>
<data_model>See get_search_details for the sourcing data model definitions (searches, agent phases, candidates, feedback, ICPs, scheduled searches, search-sequence links, posting to ATS).</data_model>
List Ats Jobs
<usecase>
List active jobs in the workspace's connected ATS (Applicant Tracking System).
Use this to find the ATS job to post sourcing candidates to with post_sourcing_candidates_to_ats. Each job includes its ATS job ID, title, department, and location.
The response also includes posting_disabled_reason — if non-null, candidates cannot currently be posted to the ATS (e.g. no ATS integration, or the integration lacks permissions) and the reason should be relayed to the user.
</usecase>
<instructions>
Args:
query: Optional title search to filter jobs (e.g. 'engineer').
limit: Maximum jobs to return (default 20, max 100).
offset: Pagination offset (default 0).
</instructions>
<response_format>{"jobs": [{"id": "4012345", "title": "Senior Backend Engineer", "department": "Engineering", "location": "London"}], "has_more": false, "posting_disabled_reason": null, "next": "Pick a job, then optionally call list_ats_stages..."}</response_format>
<examples>
List jobs matching a title:
list_ats_jobs(query="backend engineer")
List all active jobs:
list_ats_jobs()
</examples>
List Ats Stages
<usecase>
List the application stages for a job in the workspace's connected ATS.
Use this to find the stage_id to post sourcing candidates into with post_sourcing_candidates_to_ats. Providing a stage is optional — when omitted, candidates land in the ATS default stage for the job.
</usecase>
<instructions>
Args:
job_id: The ATS job ID (from list_ats_jobs). Omit to list workspace-level stages for ATSes where stages are not job-specific.
</instructions>
<response_format>{"job_id": "4012345", "stages": [{"id": "70001", "name": "Application Review"}]}</response_format>
<examples>
Stages for a job:
list_ats_stages(job_id="4012345")
</examples>
Post Sourcing Candidates To Ats
<usecase>
Post candidates from a sourcing search to the workspace's connected ATS.
Creates each candidate in the ATS (if they don't already exist there) and adds an application to the given job. The sourcing agent's reasoning for each candidate is posted as a note on the ATS profile.
This is the same action as the 'Add to ATS' button in the Metaview web app.
Maximum 50 candidates per call.
Typical flow:
1. list_sourcing_candidates to get candidate IDs from a search.
2. list_ats_jobs to find the target job (and optionally list_ats_stages for a specific stage).
3. post_sourcing_candidates_to_ats to post them.
IMPORTANT: Posting is irreversible from Metaview — candidates created in the ATS must be removed in the ATS itself. Confirm the target job with the user before posting unless they have already specified it.
</usecase>
<instructions>
Args:
search_id: The sourcing search containing the candidates.
candidate_ids: Candidate IDs to post (from list_sourcing_candidates).
job_id: The ATS job to add applications to (from list_ats_jobs).
stage_id: Optional ATS stage for the new applications (from list_ats_stages). Omit to use the job's default stage.
</instructions>
<response_format>{"search_id": "550e8400-...", "job_id": "4012345", "accepted_count": 2, "results": [{"candidate_id": "c1a2b3d4-...", "accepted": true}, {"candidate_id": "d4e5f6a7-...", "accepted": false, "error": "Candidate not found in this search"}], "next": "Posting happens asynchronously..."}</response_format>
<notes>Posting is asynchronous. Accepted candidates are queued and typically appear in the ATS within a couple of minutes. This tool does not return the ATS URL; the candidate's ATS record is linked in Metaview once posting completes.</notes>
<examples>
Post two candidates to a job's default stage:
post_sourcing_candidates_to_ats(search_id="550e8400-...", candidate_ids=["c1a2b3d4-...", "d4e5f6a7-..."], job_id="4012345")
Post into a specific stage:
post_sourcing_candidates_to_ats(search_id="550e8400-...", candidate_ids=["c1a2b3d4-..."], job_id="4012345", stage_id="70001")
</examples>
Send Sourcing Message
<usecase>
Send a message to a sourcing or research search agent.
Use this to:
- Start a new sourcing search (omit search_id) by describing who you are looking for
- Start a new research search (omit search_id, set mode='research') by describing what you want researched
- Send a follow-up message to an existing search to refine requirements, answer agent questions, or provide additional context
Do NOT use this tool to ask questions that can be answered from existing data — the agent responds asynchronously and can take 5-15 minutes. For questions about a search's ICP, calibration, feedback, or status, use get_search_details. For the candidates a search has surfaced, use list_sourcing_candidates. For metrics across searches (counts, acceptance rates, trends), use get_sourcing_analytics. Those tools answer instantly; only message the agent when you need it to DO something (search, refine, calibrate, research).
Modes:
- 'source' (default): AI recruiter that searches for candidates, surfaces profiles, runs calibration, and drafts outreach.
- 'research': General research assistant that searches the web, synthesizes information, analyzes data with charts/CSVs, and writes reports. No candidate profiles or outreach.
When no search_id is provided, a new search is created and its ID is returned. Save this ID for subsequent calls.
The agent processes messages asynchronously. After sending, poll with get_sourcing_messages to see the agent's response and any candidates it surfaces.
</usecase>
<instructions>
Args:
message: The message to send. For a new source search, describe the role, requirements, and constraints (location, experience level, etc.). For a new research search, describe the topic or question to investigate. For follow-ups, provide refinements or answer the agent's questions.
search_id: ID of an existing search. Omit to create a new search.
mode: Search mode — 'source' (default) or 'research'. Only used when creating a new search (search_id is omitted). Ignored for follow-up messages.
</instructions>
<response_format>{"search_id": "550e8400-...", "status": "message_sent", "phase": "busy", "next": "Message sent. The sourcing agent is now processing..."}</response_format>
<examples>
Start a new sourcing search:
send_sourcing_message(message="I need a senior backend engineer with Python and distributed systems experience, based in Europe")
Start a new research search:
send_sourcing_message(mode="research", message="Compare the engineering cultures and tech stacks at Stripe, Square, and Adyen")
Send a follow-up:
send_sourcing_message(search_id="550e8400-...", message="Also consider candidates with Go experience, not just Python")
</examples>
Get User Context
<usecase>
IMPORTANT: Call this tool FIRST, before any other tool.
Returns your identity, role, and what data you can access.
- workspace_name: The name of the workspace you are connected to.
- participant_id: Your contact UUID. Use "$self" in filter values instead of
this raw UUID — it is resolved automatically to your participant_id.
- user_id: Your numeric user ID.
- is_admin: Whether you have admin access.
- is_paying_plan: Whether you are on a paid plan. Free users have limited access
to transcripts, summaries, and AI fields.
- data_access: Describes exactly which conversations you can see. Read this
before drawing conclusions about completeness of query results.
</usecase>
<instructions>No parameters needed.</instructions>
<response_format>
{"organization_id": 1,
"workspace_name": "Acme Corp",
"user_id": 123,
"participant_id": "550e8400-e29b-41d4-a716-446655440000",
"is_admin": true,
"is_paying_plan": true,
"data_access": "You can see all conversations in the organization."}
</response_format>