Scenario
Your interview scorecards in Greenhouse contain structured ratings and free-text feedback across hundreds of interviews. After enough interviews, patterns emerge: which traits predict successful hires? You aggregate scorecard data, send it to Mave Agent, and get back a data-driven “success persona” — the trait profile that predicts a strong hire. Use this to calibrate interviewers and refine rubrics. Flow: GreenhouseGET /scorecards → Aggregate attributes/ratings → Filter by hire outcome → Mavera POST /mave/chat → Success trait analysis
Architecture
Code
import os, requests, time, base64
from collections import defaultdict
GH_KEY = os.environ["GREENHOUSE_API_KEY"]
MV = os.environ["MAVERA_API_KEY"]
GH_BASE = "https://harvest.greenhouse.io/v1"
gh_auth = base64.b64encode(f"{GH_KEY}:".encode()).decode()
GH_H = {"Authorization": f"Basic {gh_auth}"}
# 1. Pull recent applications with outcomes
applications = []
page = 1
while len(applications) < 300:
batch = requests.get(f"{GH_BASE}/applications",
headers=GH_H,
params={"per_page": 100, "page": page, "status": "hired"}).json()
if not batch:
break
applications.extend(batch)
page += 1
time.sleep(0.3)
rejected_apps = []
page = 1
while len(rejected_apps) < 300:
batch = requests.get(f"{GH_BASE}/applications",
headers=GH_H,
params={"per_page": 100, "page": page, "status": "rejected"}).json()
if not batch:
break
rejected_apps.extend(batch)
page += 1
time.sleep(0.3)
hired_app_ids = {a["id"] for a in applications}
rejected_app_ids = {a["id"] for a in rejected_apps}
# 2. Pull scorecards for these applications
hired_scores = defaultdict(list)
rejected_scores = defaultdict(list)
for app_id in list(hired_app_ids)[:50]:
scorecards = requests.get(f"{GH_BASE}/applications/{app_id}/scorecards",
headers=GH_H).json()
for sc in scorecards:
for attr in sc.get("attributes", []):
name = attr.get("name", "Unknown")
rating = attr.get("rating", "")
if rating:
hired_scores[name].append(rating)
time.sleep(0.25)
for app_id in list(rejected_app_ids)[:50]:
scorecards = requests.get(f"{GH_BASE}/applications/{app_id}/scorecards",
headers=GH_H).json()
for sc in scorecards:
for attr in sc.get("attributes", []):
name = attr.get("name", "Unknown")
rating = attr.get("rating", "")
if rating:
rejected_scores[name].append(rating)
time.sleep(0.25)
# 3. Build analysis context
RATING_MAP = {"definitely_not": 1, "no": 2, "mixed": 3, "yes": 4, "strong_yes": 5}
analysis_lines = []
all_attrs = set(hired_scores.keys()) | set(rejected_scores.keys())
for attr in sorted(all_attrs):
h_vals = [RATING_MAP.get(r, 3) for r in hired_scores.get(attr, [])]
r_vals = [RATING_MAP.get(r, 3) for r in rejected_scores.get(attr, [])]
h_avg = sum(h_vals) / len(h_vals) if h_vals else 0
r_avg = sum(r_vals) / len(r_vals) if r_vals else 0
delta = h_avg - r_avg
analysis_lines.append(f"- {attr}: Hired avg={h_avg:.1f} Rejected avg={r_avg:.1f} Delta={delta:+.1f}")
analysis_block = "\n".join(analysis_lines)
# 4. Mave synthesis
mave = requests.post("https://app.mavera.io/api/v1/mave/chat",
headers={"Authorization": f"Bearer {MV}", "Content-Type": "application/json"},
json={"message": f"""Analyze these interview scorecard attributes comparing hired vs rejected candidates.
SCORECARD DATA ({len(hired_app_ids)} hired, {len(rejected_app_ids)} rejected applications analyzed):
{analysis_block}
Produce:
1. Top 5 traits that predict successful hires (highest delta)
2. Traits that DON'T predict success (low delta — possible interviewer bias)
3. Recommended interview rubric adjustments
4. Calibration notes for interviewers
5. A "success persona" profile summarizing the ideal candidate"""}).json()
print("=== Hiring Success Persona ===")
print(mave.get("content", "")[:2000])
const GH_KEY = process.env.GREENHOUSE_API_KEY;
const MV = process.env.MAVERA_API_KEY;
const GH_BASE = "https://harvest.greenhouse.io/v1";
const GH_H = { Authorization: `Basic ${btoa(`${GH_KEY}:`)}` };
const RATING_MAP = { definitely_not: 1, no: 2, mixed: 3, yes: 4, strong_yes: 5 };
async function ghGet(path, params = {}) {
const qs = new URLSearchParams(params).toString();
const res = await fetch(`${GH_BASE}${path}?${qs}`, { headers: GH_H });
if (res.status === 429) { await new Promise((r) => setTimeout(r, 10000)); return ghGet(path, params); }
if (!res.ok) throw new Error(`GH ${res.status}`);
return res.json();
}
async function fetchApps(status, max = 300) {
const apps = [];
let page = 1;
while (apps.length < max) {
const batch = await ghGet("/applications", { per_page: 100, page, status });
if (!batch.length) break;
apps.push(...batch);
page++;
await new Promise((r) => setTimeout(r, 300));
}
return apps;
}
// 1. Applications
const hiredApps = await fetchApps("hired");
const rejectedApps = await fetchApps("rejected");
// 2. Scorecards
async function collectScores(appIds) {
const scores = {};
for (const id of appIds.slice(0, 50)) {
const scorecards = await ghGet(`/applications/${id}/scorecards`);
for (const sc of scorecards) {
for (const attr of sc.attributes || []) {
const name = attr.name || "Unknown";
if (attr.rating) (scores[name] ??= []).push(attr.rating);
}
}
await new Promise((r) => setTimeout(r, 250));
}
return scores;
}
const hiredScores = await collectScores(hiredApps.map((a) => a.id));
const rejectedScores = await collectScores(rejectedApps.map((a) => a.id));
// 3. Analysis
const allAttrs = new Set([...Object.keys(hiredScores), ...Object.keys(rejectedScores)]);
const lines = [...allAttrs].sort().map((attr) => {
const hVals = (hiredScores[attr] || []).map((r) => RATING_MAP[r] || 3);
const rVals = (rejectedScores[attr] || []).map((r) => RATING_MAP[r] || 3);
const hAvg = hVals.length ? hVals.reduce((s, v) => s + v, 0) / hVals.length : 0;
const rAvg = rVals.length ? rVals.reduce((s, v) => s + v, 0) / rVals.length : 0;
return `- ${attr}: Hired avg=${hAvg.toFixed(1)} Rejected avg=${rAvg.toFixed(1)} Delta=${(hAvg - rAvg) > 0 ? "+" : ""}${(hAvg - rAvg).toFixed(1)}`;
});
// 4. Mave
const mave = await fetch("https://app.mavera.io/api/v1/mave/chat", {
method: "POST",
headers: { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" },
body: JSON.stringify({
message: `Analyze scorecard attributes (${hiredApps.length} hired, ${rejectedApps.length} rejected):\n\n${lines.join("\n")}\n\nProduce: 1) Top 5 predictive traits 2) Non-predictive traits 3) Rubric adjustments 4) Calibration notes 5) Success persona`,
}),
}).then((r) => r.json());
console.log("=== Hiring Success Persona ===");
console.log((mave.content || "").slice(0, 2000));
Example Output
=== Hiring Success Persona ===
## Top 5 Predictive Traits (highest delta hired vs rejected)
1. Problem Solving (+1.8) — Strongest signal. Hired candidates avg 4.6 vs 2.8.
2. Communication (+1.4) — Clear articulation correlates with success.
3. Technical Depth (+1.2) — Deep expertise matters more than breadth.
4. Ownership Mentality (+1.1) — Proactive candidates outperform.
5. Collaboration (+0.9) — Team fit is a real predictor, not just bias.
## Non-Predictive Traits (possible interviewer bias)
- "Culture Fit" (+0.2) — Nearly identical scores. Likely subjective.
- "Enthusiasm" (+0.1) — Extroverts score higher but don't hire better.
Recommendation: Remove or redefine as "Motivation Alignment."
## Success Persona
Senior-level IC. Strong problem solver who communicates decisions clearly.
Deep rather than broad technical skills. Takes ownership of ambiguous problems.
Collaborative but doesn't need consensus to move forward.
Error Handling
Scorecard permissions
Scorecard permissions
Scorecards require the
Scorecards permission on your API key. Without it, you get 403. Update in Greenhouse → Dev Center.Rating value mapping
Rating value mapping
Greenhouse uses string ratings:
definitely_not, no, mixed, yes, strong_yes. The code maps these to 1-5. Custom rating systems need manual mapping.Large scorecard volumes
Large scorecard volumes
50 applications × N scorecards each generates many API calls. The 50 req/10s limit means you may need to batch over minutes for 200+ applications.