> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Employee Experience → Internal Personas

> Create internal personas from Qualtrics employee engagement data and test company communications with Mavera Focus Groups

### Scenario

Your quarterly employee engagement survey (eNPS + engagement questions) reveals distinct workforce segments — the Engaged Advocate who scores everything 9+, the Quiet Quitter giving 5-6 across the board, the New Hire still forming opinions. You need to test internal communications (policy announcements, benefit changes, company updates) against these segments before sending. This job pulls engagement data, creates internal personas matching each segment, then runs Focus Groups testing draft comms against each persona type.

**Flow:** Qualtrics eNPS + engagement export → Segment by engagement tier → Create internal personas (Engaged Advocate, Quiet Quitter, New Hire) → `POST /api/v1/focus-groups` testing internal comms → Segment-validated messaging

### Architecture

```mermaid theme={"dark"}
flowchart LR
A["Employee Survey export"] --> B["Segment by eNPS + tenure"] --> C["POST /api/v1/personas"] --> D["POST /api/v1/focus-groups"] --> E["Per-segment comms recommendations"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, csv, io, zipfile, requests, time
  from collections import defaultdict

  QT = os.environ["QUALTRICS_TOKEN"]
  DC = os.environ["QUALTRICS_DC"]
  MV = os.environ["MAVERA_API_KEY"]
  Q_BASE = f"https://{DC}.qualtrics.com/API/v3"
  MB = "https://app.mavera.io/api/v1"
  Q_H = {"X-API-TOKEN": QT, "Content-Type": "application/json"}
  MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  SURVEY_ID = os.environ.get("ENPS_SURVEY_ID", "SV_xxxxx")

  # 1. Export (same async pattern)
  export = requests.post(f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses",
      headers=Q_H, json={"format": "csv"}).json()
  pid = export["result"]["progressId"]
  file_id = None
  for _ in range(60):
      time.sleep(5)
      s = requests.get(f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses/{pid}",
          headers=Q_H).json()
      if s["result"].get("percentComplete") == 100:
          file_id = s["result"]["fileId"]; break

  zip_data = requests.get(
      f"{Q_BASE}/surveys/{SURVEY_ID}/export-responses/{file_id}/file",
      headers=Q_H).content
  with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
      csv_name = [n for n in zf.namelist() if n.endswith(".csv")][0]
      with zf.open(csv_name) as f:
          rows = list(csv.DictReader(io.TextIOWrapper(f, encoding="utf-8-sig")))
  responses = [r for r in rows if r.get("Finished") == "1"]
  print(f"Employee responses: {len(responses)}")

  # 2. Segment employees
  ENPS_COL = os.environ.get("ENPS_COLUMN", "eNPS")
  TENURE_COL = os.environ.get("TENURE_COLUMN", "Tenure_Months")

  segments = {"engaged_advocate": [], "passive_performer": [],
              "quiet_quitter": [], "new_hire": [], "at_risk_veteran": []}

  for r in responses:
      try:
          enps = float(r.get(ENPS_COL, 0) or 0)
      except ValueError:
          continue
      try:
          tenure = float(r.get(TENURE_COL, 12) or 12)
      except ValueError:
          tenure = 12

      if tenure < 6:
          segments["new_hire"].append(r)
      elif enps >= 9:
          segments["engaged_advocate"].append(r)
      elif enps >= 7:
          segments["passive_performer"].append(r)
      elif enps >= 4 and tenure > 24:
          segments["at_risk_veteran"].append(r)
      else:
          segments["quiet_quitter"].append(r)

  for name, members in segments.items():
      print(f"  {name}: {len(members)}")

  # 3. Create internal personas
  PERSONA_PROFILES = {
      "engaged_advocate": {
          "name": "QX Internal: Engaged Advocate",
          "desc": "eNPS 9-10. Enthusiastic about company direction. Shares updates proactively. First to volunteer for initiatives.",
          "mindset": "Invested, energized, wants to be recognized and involved",
      },
      "passive_performer": {
          "name": "QX Internal: Passive Performer",
          "desc": "eNPS 7-8. Doing their job well but not engaged beyond requirements. Reads comms but rarely responds.",
          "mindset": "Content but disengaged from culture. Needs a reason to care more.",
      },
      "quiet_quitter": {
          "name": "QX Internal: Quiet Quitter",
          "desc": "eNPS 0-6. Doing minimum required. Skeptical of corporate comms. May be passively job searching.",
          "mindset": "Disillusioned. Reads announcements looking for reasons to leave or stay.",
      },
      "new_hire": {
          "name": "QX Internal: New Hire",
          "desc": "Under 6 months tenure. Still forming opinions. Observing culture. Comparing to previous employer.",
          "mindset": "Observant, optimistic but cautious. Every comm shapes first impression.",
      },
      "at_risk_veteran": {
          "name": "QX Internal: At-Risk Veteran",
          "desc": "2+ years tenure, eNPS 4-6. Was once engaged. Something shifted. Institutional knowledge at risk.",
          "mindset": "Nostalgic for how things were. Needs to see positive change, not hear about it.",
      },
  }

  persona_ids = {}
  for seg_key, profile in PERSONA_PROFILES.items():
      members = segments.get(seg_key, [])
      if not members:
          continue
      depts = list({m.get("Department", "") for m in members if m.get("Department")})[:5]

      p = requests.post(f"{MB}/personas", headers=MV_H, json={
          "name": profile["name"],
          "description": f"{profile['desc']} N={len(members)}. Departments: {', '.join(depts[:3])}.",
          "psychographic": {"mindset": profile["mindset"], "engagement_tier": seg_key},
      })
      p.raise_for_status()
      persona_ids[seg_key] = p.json()["id"]
      time.sleep(0.3)

  # 4. Test internal communication
  DRAFT_COMMS = [
      {"type": "Policy Change", "text": "Starting Q2, we're moving to a hybrid model: 3 days in-office, 2 remote. This balances collaboration and flexibility based on team feedback."},
      {"type": "Benefit Update", "text": "We're expanding mental health coverage to include 12 therapy sessions/year (up from 6) and adding a $500 wellness stipend."},
      {"type": "Reorg Announcement", "text": "To accelerate our enterprise strategy, we're combining the Sales and CS teams under a new Chief Revenue Officer starting April 1."},
  ]

  for comm in DRAFT_COMMS:
      fg = requests.post(f"{MB}/focus-groups", headers=MV_H, json={
          "name": f"Internal Comms: {comm['type']}",
          "persona_ids": list(persona_ids.values()),
          "context": f"Your company just sent this internal announcement:\n\n\"{comm['text']}\"",
          "questions": [
              "What is your immediate reaction to this announcement?",
              "What questions does this raise that aren't answered?",
              "How does this affect your engagement or morale?",
              "If you were writing this announcement, what would you change?",
          ],
          "responses_per_persona": 2,
      }).json()

      for _ in range(20):
          time.sleep(5)
          result = requests.get(f"{MB}/focus-groups/{fg['id']}",
              headers=MV_H).json()
          if result.get("status") == "completed":
              break

      print(f"\n{'='*50}")
      print(f"COMMS TEST: {comm['type']}")
      print(f"{'='*50}")
      for resp in result.get("responses", []):
          print(f"\n[{resp.get('persona_name', '?')}] {resp.get('question', '')[:50]}...")
          print(f"  → {resp.get('answer', '')[:250]}")
  ```

  ```javascript JavaScript theme={"dark"}
  const QT = process.env.QUALTRICS_TOKEN;
  const DC = process.env.QUALTRICS_DC;
  const MV = process.env.MAVERA_API_KEY;
  const Q_BASE = `https://${DC}.qualtrics.com/API/v3`;
  const MB = "https://app.mavera.io/api/v1";
  const qH = { "X-API-TOKEN": QT, "Content-Type": "application/json" };
  const mvH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const SURVEY_ID = process.env.ENPS_SURVEY_ID || "SV_xxxxx";

  // 1. Export + parse (same async pattern)
  const JSZip = (await import("jszip")).default;
  const exp = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses`,
    { method: "POST", headers: qH, body: JSON.stringify({ format: "csv" }) }).then(r => r.json());
  let fileId = null;
  for (let i = 0; i < 60; i++) {
    await new Promise(r => setTimeout(r, 5000));
    const s = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses/${exp.result.progressId}`,
      { headers: qH }).then(r => r.json());
    if (s.result?.percentComplete === 100) { fileId = s.result.fileId; break; }
  }
  const zipBuf = await fetch(`${Q_BASE}/surveys/${SURVEY_ID}/export-responses/${fileId}/file`,
    { headers: qH }).then(r => r.arrayBuffer());
  const zip = await JSZip.loadAsync(zipBuf);
  const csvFile = Object.keys(zip.files).find(n => n.endsWith(".csv"));
  const csvText = await zip.files[csvFile].async("string");
  const lines = csvText.split("\n");
  const hdrs = lines[0].split(",").map(h => h.replace(/"/g, "").trim());
  const responses = lines.slice(3).filter(l => l.trim()).map(line => {
    const vals = line.match(/(".*?"|[^,]*)/g) || [];
    const obj = {};
    hdrs.forEach((h, i) => { obj[h] = (vals[i] || "").replace(/"/g, "").trim(); });
    return obj;
  }).filter(r => r.Finished === "1");

  // 2. Segment
  const segs = { engaged_advocate: [], passive_performer: [], quiet_quitter: [], new_hire: [], at_risk_veteran: [] };
  const ENPS_COL = process.env.ENPS_COLUMN || "eNPS";
  const TENURE_COL = process.env.TENURE_COLUMN || "Tenure_Months";
  for (const r of responses) {
    const enps = parseFloat(r[ENPS_COL] || "0");
    const tenure = parseFloat(r[TENURE_COL] || "12");
    if (isNaN(enps)) continue;
    if (tenure < 6) segs.new_hire.push(r);
    else if (enps >= 9) segs.engaged_advocate.push(r);
    else if (enps >= 7) segs.passive_performer.push(r);
    else if (enps >= 4 && tenure > 24) segs.at_risk_veteran.push(r);
    else segs.quiet_quitter.push(r);
  }

  // 3. Create personas
  const PROFILES = {
    engaged_advocate: { name: "QX: Engaged Advocate", desc: "eNPS 9-10. Enthusiastic.", mindset: "Invested, energized" },
    passive_performer: { name: "QX: Passive Performer", desc: "eNPS 7-8. Doing well but not engaged.", mindset: "Content, disengaged" },
    quiet_quitter: { name: "QX: Quiet Quitter", desc: "eNPS 0-6. Minimum effort.", mindset: "Disillusioned, skeptical" },
    new_hire: { name: "QX: New Hire", desc: "<6 months. Still forming opinions.", mindset: "Observant, cautious" },
    at_risk_veteran: { name: "QX: At-Risk Veteran", desc: "2+ years, declining engagement.", mindset: "Nostalgic, needs change" },
  };

  const personaIds = {};
  for (const [key, profile] of Object.entries(PROFILES)) {
    if (!segs[key].length) continue;
    const p = await fetch(`${MB}/personas`, { method: "POST", headers: mvH,
      body: JSON.stringify({ name: profile.name, description: `${profile.desc} N=${segs[key].length}.`,
        psychographic: { mindset: profile.mindset, tier: key } }),
    }).then(r => r.json());
    personaIds[key] = p.id;
    await new Promise(r => setTimeout(r, 300));
  }

  // 4. Test comms
  const COMMS = [
    { type: "Policy Change", text: "Starting Q2, hybrid model: 3 days office, 2 remote." },
    { type: "Benefit Update", text: "Expanding mental health coverage to 12 sessions/year + $500 wellness stipend." },
    { type: "Reorg", text: "Combining Sales and CS under a new CRO starting April 1." },
  ];

  for (const comm of COMMS) {
    const fg = await fetch(`${MB}/focus-groups`, { method: "POST", headers: mvH,
      body: JSON.stringify({
        name: `Internal: ${comm.type}`, persona_ids: Object.values(personaIds),
        context: `Company announcement:\n"${comm.text}"`,
        questions: [ "Immediate reaction?", "What questions aren't answered?",
          "How does this affect your engagement?", "What would you change about this announcement?" ],
        responses_per_persona: 2,
      }),
    }).then(r => r.json());

    let result;
    for (let i = 0; i < 20; i++) {
      await new Promise(r => setTimeout(r, 5000));
      result = await fetch(`${MB}/focus-groups/${fg.id}`, { headers: mvH }).then(r => r.json());
      if (result.status === "completed") break;
    }
    console.log(`\n${"=".repeat(50)}\nCOMMS: ${comm.type}\n${"=".repeat(50)}`);
    for (const resp of result.responses || [])
      console.log(`[${resp.persona_name}] ${(resp.answer || "").slice(0, 200)}\n`);
  }
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
==================================================
COMMS TEST: Reorg Announcement
==================================================

[QX: Engaged Advocate] What is your immediate reaction?
  → I trust leadership's judgment, but I want to understand what
    "accelerate enterprise strategy" means for my team specifically.
    Who's my new manager? What happens to my current projects?

[QX: Quiet Quitter] What is your immediate reaction?
  → Another reorg. Third in 18 months. The last one was supposed to
    "accelerate growth" too. I'll believe it when I see it. Until
    then, I'm updating my LinkedIn.

[QX: New Hire] How does this affect your engagement?
  → I literally just learned everyone's names. Now the org chart is
    changing? This makes me wonder if the company is stable. I need
    my manager to explain what this means for my role specifically.

[QX: At-Risk Veteran] What would you change?
  → Lead with "here's what stays the same." I need to know my job,
    my team, and my projects are safe before I can process the big
    picture strategy. Also — who decided this? Was anyone consulted?
```

### Error Handling

<AccordionGroup>
  <Accordion title="eNPS column naming">Employee surveys use custom column names. Set `ENPS_COLUMN` and `TENURE_COLUMN` env vars to match your survey. Default: `eNPS` and `Tenure_Months`.</Accordion>
  <Accordion title="Small segment sizes">Some segments (e.g., At-Risk Veteran) may have fewer than 10 members. Personas still work — they represent the archetype, not the statistical average.</Accordion>
  <Accordion title="Sequential focus groups">Testing 3 announcements × 5 personas sequentially takes 5-10 minutes. For faster results, reduce to the 3 most critical personas per announcement.</Accordion>
</AccordionGroup>
