# 29 — Voice Note Transcription & CRM Integration

**Status:** Planning | **Depends on:** 03 (Self-Hosted AI), 16 (React Native Mobile)
**Estimated effort:** 90 hours | **Phase:** After AI layer + mobile | **Type:** Mobile feature + AI integration

---

## OVERVIEW

Luxury Presence's Presence CoPilot lets agents record voice notes that AI transcribes and extracts into structured CRM data. This module adds voice capture (mobile + web), transcription via self-hosted Whisper or cloud API, AI entity extraction, and auto-population of CRM fields. Agents review and confirm before commit (human in the loop per operational rules).

**What it replaces:** Handwritten notes, dictated voice memos that never become CRM data. Improves mobile experience with voice-first input for busy agents.

**What it adds:**
- Voice recording on mobile (React Native, instruction 16) and web (MediaRecorder API)
- Transcription (self-hosted Whisper from instruction 03, or Whisper/Deepgram APIs)
- AI entity extraction (names, addresses, prices, dates, preferences, action items)
- Auto-CRM population (create/update clients, interactions, tasks, properties, showings)
- Smart categorization (post-showing, client call, market observation, personal reminder)
- Full-text search + playback (audio synced to transcript)
- Integration with CRM, showings, transactions, calendar

**Why this matters:** Voice is 3x faster than typing for busy agents. Post-showing notes are critical for follow-up and compliance, but agents rarely log them (friction). Voice notes remove friction. AI extraction turns unstructured audio into structured CRM data (client names, showing feedback, next steps). Integrating showing feedback directly into deal file improves transaction outcomes.

---

## DATABASE SCHEMA

### Table: voice_notes

Audio files, transcripts, and metadata.

```sql
CREATE TABLE voice_notes (
  id INT PRIMARY KEY AUTO_INCREMENT,
  tenant_id INT,
  user_id INT REFERENCES admins(id),

  -- Audio file reference
  audio_file_url VARCHAR(500),
  audio_file_size INT,
  audio_duration_seconds INT,
  audio_format VARCHAR(20),

  -- Transcription
  transcript LONGTEXT,
  transcript_language VARCHAR(10) DEFAULT 'en',
  transcript_confidence DECIMAL(4,3),

  -- Metadata
  title VARCHAR(255),
  description TEXT,

  -- Context (what triggered this note)
  context_type VARCHAR(50),
  context_data JSON,

  -- Examples:
  -- { "type": "post_showing", "property_id": 123, "showing_id": 456, "buyer_name": "John" }
  -- { "type": "client_call", "client_id": 789, "duration": "12 min" }
  -- { "type": "market_observation", "market": "Carmel-by-the-Sea" }
  -- { "type": "personal_reminder", "due_date": "2026-04-15" }

  -- Categorization (auto-assigned, agent can override)
  category VARCHAR(50),

  -- Status: 'recording', 'transcribing', 'awaiting_review', 'reviewed', 'extracted', 'archived'
  status VARCHAR(50) DEFAULT 'recording',

  -- Extraction status
  extracted_entities_count INT DEFAULT 0,
  extraction_confirmed_at DATETIME,

  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  FOREIGN KEY (tenant_id) REFERENCES tenants(id),
  FOREIGN KEY (user_id) REFERENCES admins(id),
  INDEX (tenant_id, user_id),
  INDEX (status),
  INDEX (category),
  INDEX (created_at),
  INDEX (context_type)
);
```

### Table: voice_extractions

Structured entities extracted from voice notes by AI.

```sql
CREATE TABLE voice_extractions (
  id INT PRIMARY KEY AUTO_INCREMENT,
  tenant_id INT,
  voice_note_id INT REFERENCES voice_notes(id),

  -- Entity type: 'person_name', 'property_address', 'price_point', 'date',
  --              'timeline', 'feature_preference', 'objection', 'next_action', 'contact_info'
  entity_type VARCHAR(100),

  -- Extracted value
  entity_value VARCHAR(500),

  -- Confidence (0-1)
  confidence DECIMAL(4,3),

  -- Transcript snippet (context)
  source_text VARCHAR(1000),
  start_time_ms INT,
  end_time_ms INT,

  -- Status: 'extracted', 'confirmed', 'rejected', 'applied'
  status VARCHAR(50) DEFAULT 'extracted',

  -- Which CRM field this maps to (if applicable)
  mapped_field VARCHAR(100),
  mapped_table VARCHAR(100),

  -- Applied to CRM (if confirmed)
  applied_to_record_id INT,
  applied_at DATETIME,

  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  FOREIGN KEY (tenant_id) REFERENCES tenants(id),
  FOREIGN KEY (voice_note_id) REFERENCES voice_notes(id),
  INDEX (tenant_id, voice_note_id),
  INDEX (status),
  INDEX (entity_type)
);
```

### Table: voice_note_tags

User-defined tags for organizing and searching voice notes.

```sql
CREATE TABLE voice_note_tags (
  id INT PRIMARY KEY AUTO_INCREMENT,
  tenant_id INT,
  voice_note_id INT REFERENCES voice_notes(id),

  -- Tag: 'follow-up-needed', 'price-negotiation', 'inspection-issue', 'closing-date', 'appraisal', 'financing', etc.
  tag VARCHAR(100),

  -- Auto-assigned or manual
  auto_assigned TINYINT DEFAULT 0,

  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

  FOREIGN KEY (tenant_id) REFERENCES tenants(id),
  FOREIGN KEY (voice_note_id) REFERENCES voice_notes(id),
  INDEX (tenant_id, voice_note_id),
  INDEX (tag)
);
```

### Table: voice_note_actions

Extracted action items from voice notes.

```sql
CREATE TABLE voice_note_actions (
  id INT PRIMARY KEY AUTO_INCREMENT,
  tenant_id INT,
  voice_note_id INT REFERENCES voice_notes(id),
  user_id INT REFERENCES admins(id),

  -- Action description
  action_text VARCHAR(500),

  -- Due date (if mentioned)
  due_date DATE,

  -- Status: 'open', 'in-progress', 'completed', 'cancelled'
  status VARCHAR(50) DEFAULT 'open',

  -- Source confidence (how sure AI was this was an action item)
  confidence DECIMAL(4,3),

  completed_at DATETIME,

  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  FOREIGN KEY (tenant_id) REFERENCES tenants(id),
  FOREIGN KEY (voice_note_id) REFERENCES voice_notes(id),
  FOREIGN KEY (user_id) REFERENCES admins(id),
  INDEX (tenant_id, user_id),
  INDEX (status),
  INDEX (due_date)
);
```

---

## API ENDPOINTS

### POST /api/voice_notes.php?action=uploadVoiceNote

Upload audio file and start transcription.

**Request (multipart/form-data):**
```
audio_file: [binary WAV/MP3/AAC file]
context_type: "post_showing" | "client_call" | "market_observation" | "personal_reminder"
context_data: { "property_id": 123, "showing_id": 456, ... }
title: "Showing feedback - 123 Ocean Ave"
```

**Response:**
```json
{
  "success": true,
  "data": {
    "voice_note_id": 789,
    "status": "transcribing",
    "audio_url": "https://fogbreak.io/audio/voice_notes/789.wav",
    "estimated_transcription_time": "30 seconds"
  },
  "message": "Voice note uploaded. Transcription in progress."
}
```

**Implementation:**
- Save audio file to secure storage (fogbreak.io/audio/voice_notes/{id}.{ext})
- Create voice_notes record with status='recording'
- Queue transcription job:
  - Try self-hosted Whisper first (instruction 03)
  - Fallback to Whisper API or Deepgram if unavailable
- Return voice_note_id + tracking URL
- Set status='transcribing'

---

### GET /api/voice_notes.php?action=getTranscript

Fetch transcript for a voice note.

**Query params:** `voice_note_id`

**Response:**
```json
{
  "success": true,
  "data": {
    "voice_note_id": 789,
    "transcript": "John walked into the property... [full transcript]",
    "status": "awaiting_review",
    "confidence": 0.94,
    "duration_seconds": 87,
    "transcript_chunks": [
      { "time_ms": 0, "text": "John walked into the property..." },
      { "time_ms": 5000, "text": "Great light in the master..." }
    ]
  }
}
```

**Implementation:**
- Query voice_notes
- Return full transcript + confidence + chunks (for synced playback)

---

### GET /api/voice_notes.php?action=getExtractions

Fetch extracted entities awaiting confirmation.

**Query params:** `voice_note_id`

**Response:**
```json
{
  "success": true,
  "data": {
    "extractions": [
      {
        "id": 1001,
        "entity_type": "person_name",
        "entity_value": "Jane Smith",
        "confidence": 0.98,
        "source_text": "The buyer's agent Jane Smith said...",
        "status": "extracted",
        "suggested_action": "Create interaction with Jane Smith"
      },
      {
        "id": 1002,
        "entity_type": "price_point",
        "entity_value": "$5,200,000",
        "confidence": 0.95,
        "source_text": "They're willing to go up to 5.2 million.",
        "status": "extracted",
        "suggested_action": "Update buyer needs max_price"
      },
      {
        "id": 1003,
        "entity_type": "next_action",
        "entity_value": "Schedule second showing",
        "confidence": 0.87,
        "source_text": "Let's schedule a second showing for next week.",
        "status": "extracted",
        "suggested_action": "Create task: Schedule showing"
      }
    ],
    "total": 8
  }
}
```

**Implementation:**
- Query voice_extractions for voice_note_id where status IN ('extracted', 'awaiting_review')
- Rank by confidence DESC
- Return suggested_action for each (human review step)

---

### POST /api/voice_notes.php?action=confirmExtraction

Confirm extracted entity and apply to CRM.

**Request:**
```json
{
  "extraction_id": 1002,
  "action": "apply",
  "mapped_field": "price_range_max",
  "mapped_table": "buyer_needs",
  "record_id": 456
}
```

**Implementation:**
- Validate extraction_id
- Set status='confirmed'
- Call applyCRMExtraction({entity_type, entity_value, mapped_field, mapped_table, record_id})
- Log change to audit trail
- Set applied_at = NOW()

---

### POST /api/voice_notes.php?action=rejectExtraction

Reject an extracted entity.

**Request:**
```json
{
  "extraction_id": 1003,
  "reason": "False positive - they said 'no, not interested'"
}
```

**Implementation:**
- Set status='rejected'
- Log rejection reason for training data (future: improve model)

---

### GET /api/voice_notes.php?action=searchVoiceNotes

Full-text search across transcripts + tags.

**Query params:** `q` (search query), `limit` (default 20), `category`, `context_type`, `user_id`

**Response:**
```json
{
  "success": true,
  "data": {
    "results": [
      {
        "voice_note_id": 789,
        "title": "Showing feedback - 123 Ocean Ave",
        "context_type": "post_showing",
        "category": "inspection-issue",
        "created_at": "2026-03-28T14:32:00Z",
        "snippet": "...roof repair needed in the west wing...",
        "duration_seconds": 87,
        "confidence": 0.94
      }
    ],
    "total": 3
  }
}
```

**Implementation:**
- Full-text search on voice_notes.transcript + tags
- Filter by category, context_type, user_id (if provided)
- Return paginated results with snippet

---

### GET /api/voice_notes.php?action=getVoiceNotesByClient

Get all voice notes linked to a client.

**Query params:** `client_id`

**Response:**
```json
{
  "success": true,
  "data": {
    "notes": [
      {
        "voice_note_id": 789,
        "title": "Client call with John",
        "context_type": "client_call",
        "created_at": "2026-03-28T14:32:00Z",
        "duration_seconds": 45,
        "transcript_preview": "John mentioned he needs..."
      }
    ],
    "total": 7
  }
}
```

**Implementation:**
- Query voice_notes where context_data->"$.client_id" = client_id
- Return paginated list ordered by created_at DESC

---

### POST /api/voice_notes.php?action=tagVoiceNote

Add user-defined tag to voice note.

**Request:**
```json
{
  "voice_note_id": 789,
  "tag": "financing-contingency"
}
```

**Implementation:**
- Insert into voice_note_tags
- Update voice_notes.status if status='awaiting_review' → status='reviewed'

---

### GET /api/voice_notes.php?action=getActionItems

Get action items extracted from voice notes.

**Query params:** `user_id` (filter by owner), `status` (default 'open'), `due_date_min`, `due_date_max`

**Response:**
```json
{
  "success": true,
  "data": {
    "actions": [
      {
        "action_id": 5001,
        "voice_note_id": 789,
        "action_text": "Schedule second showing",
        "due_date": "2026-04-05",
        "status": "open",
        "confidence": 0.92,
        "created_at": "2026-03-28T14:32:00Z"
      }
    ],
    "total": 12,
    "overdue": 2
  }
}
```

---

### PUT /api/voice_notes.php?action=completeActionItem

Mark action item as completed.

**Request:**
```json
{
  "action_item_id": 5001,
  "completed_at": "2026-04-02T10:30:00Z"
}
```

**Implementation:**
- Set status='completed', completed_at=NOW()
- Optional: log as task_completed in daily_action_feed.js

---

## IMPLEMENTATION STEPS

### Phase 1: Voice Recording & Transcription (Week 1-2)

1. **Create api/voice_notes.php** (new module)
   - Auto-migrate tables on first request
   - Implement uploadVoiceNote, getTranscript endpoints
   - Standard auth checks

2. **Implement mobile voice recording** (React Native, instruction 16)
   - Use `react-native-audio-toolkit` or similar
   - Record button + visualization (waveform)
   - Post-recording preview + ability to re-record
   - Upload to /api/voice_notes.php?action=uploadVoiceNote
   - Track transcription progress (polling)

3. **Implement web voice recording** (js/voice-recorder.js)
   - Use MediaRecorder API (Chrome, Firefox, Safari)
   - Record as WebM/Opus (compress)
   - Post to uploadVoiceNote
   - Progress indicator while transcribing

4. **Add transcription worker** (backend, api/cron.php new job)
   - Job: `voice_note_transcribe`
   - Run every 5 minutes
   - Detect voice_notes with status='transcribing'
   - Call self-hosted Whisper (instruction 03):
     ```bash
     curl -X POST http://localhost:8000/v1/audio/transcriptions \
       -H "Content-Type: multipart/form-data" \
       -F "file=@/path/to/audio.wav" \
       -F "model=base"
     ```
   - Fallback to Whisper API (OpenAI) if self-hosted unavailable
   - Update transcript, confidence, status='awaiting_review'
   - Start extraction job

---

### Phase 2: Entity Extraction & CRM Integration (Week 2-3)

5. **Create AI extraction pipeline** (api/voice_notes.php → extractEntities)
   - Use LLM from instruction 03 (Llama 3.3 70B or Mistral 7B)
   - Prompt:
     ```
     Analyze this real estate showing note transcript.
     Extract all entities: names (people, companies), addresses, prices, dates, buyer preferences, objections, next actions, contact info.
     For each entity: provide type, value, confidence (0-1), source quote.
     Format as JSON array.
     ```
   - Use LangGraph (instruction 03) for structured output
   - Insert results into voice_extractions
   - Rank by confidence

6. **Implement confirmation UI** (fogbreak.html new tab, or mobile alert)
   - Show extracted entities with confidence scores
   - Allow confirm/reject/edit
   - Suggested action for each (create client, update price, schedule task)
   - Batch apply (apply multiple at once)

7. **Create applyCRMExtraction function** (applies to CRM)
   ```php
   function applyCRMExtraction($extraction) {
     switch ($extraction['entity_type']) {
       case 'person_name':
         // Create/update client
         // If context is 'post_showing', create interaction
         break;
       case 'price_point':
         // Update buyer_needs.max_price (if in context)
         break;
       case 'date':
         // If in context 'post_showing', set showing.feedback_date
         break;
       case 'next_action':
         // Create task in tasks table
         break;
       // ... more entity types
     }
   }
   ```

8. **Add to daily_action_feed.js**
   - Voice notes awaiting confirmation as action item
   - Extracted action items as tasks
   - Highlight overdue items

---

### Phase 3: Categorization & Organization (Week 3)

9. **Implement smart categorization** (api/voice_notes.php → categorizeNote)
   - Use context_type to pre-categorize
   - post_showing → inspect for inspection issues, objections, financing concerns
   - client_call → track buyer preferences, timeline, price feedback
   - market_observation → track market trends, competitor activity
   - personal_reminder → simple task
   - Categories: 'inspection-issue', 'financing', 'appraisal', 'closing-timeline', 'objection', 'follow-up-needed', etc.
   - Auto-assign tags (AI inference)
   - Agent can override

10. **Implement search & playback** (js/voice-player.js new module)
    - Full-text search on transcripts
    - Results show snippet with timestamp
    - Click snippet → jump to timestamp in playback
    - Playback synced to transcript (highlight current word)
    - Speed controls (0.75x, 1x, 1.25x, 1.5x)

---

### Phase 4: Integration with CRM & Transactions (Week 4)

11. **Integrate with showings.php**
    - On showing completed, suggest voice note recording (mobile UI)
    - Voice note context_type='post_showing' auto-links to showing_id
    - Show link in showing detail: "Feedback recorded"
    - Extract feedback → populate showing.feedback_text + property notes

12. **Integrate with transactions.php**
    - Voice notes with context_type='client_call' linked to transaction
    - Extract dates → update critical path deadlines
    - Extract objections → log in deal notes
    - Extracted action items appear in transaction task list

13. **Integrate with admin.php (CRM)**
    - Client detail page shows all voice notes (filtered by client_id)
    - Agent detail page shows agent's voice notes
    - Create interaction from voice note (one-click)

14. **Integrate with calendar.php**
    - Extracted dates with due_date → create calendar events
    - Voice note action items → calendar reminders

---

### Phase 5: Advanced Features & Analytics (Week 5)

15. **Add voice note analytics** (fogbreak.html dashboard)
    - Metrics: notes recorded, avg duration, extraction accuracy
    - Most common entities extracted (names, prices, dates)
    - Agent productivity (notes per agent per week)
    - Time saved metric (notes vs manual entry)

16. **Implement usage insights**
    - Track which agents use voice notes most
    - Which context types are most common
    - Which extracted entities are most valuable
    - Adjust AI prompts based on extraction accuracy

17. **Add batch audio processing** (for large teams)
    - Bulk upload audio files (for syncing from other systems)
    - Batch transcription queue

---

## DATABASE QUERIES (PDO Examples)

### Insert voice note
```php
$stmt = $db->prepare("
  INSERT INTO voice_notes (tenant_id, user_id, audio_file_url, audio_file_size,
                           audio_duration_seconds, context_type, context_data,
                           title, status, created_at)
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'recording', NOW())
");
$context_data = json_encode($context);
$stmt->execute([$tenant_id, $user_id, $audio_url, $file_size, $duration, $context_type, $context_data, $title]);
```

### Get extractions for review
```php
$stmt = $db->prepare("
  SELECT id, entity_type, entity_value, confidence, source_text, start_time_ms, end_time_ms
  FROM voice_extractions
  WHERE tenant_id = ? AND voice_note_id = ? AND status IN ('extracted', 'awaiting_review')
  ORDER BY confidence DESC
");
$stmt->execute([$tenant_id, $voice_note_id]);
$extractions = $stmt->fetchAll(PDO::FETCH_ASSOC);
```

### Search voice notes
```php
$stmt = $db->prepare("
  SELECT id, title, context_type, category, created_at, audio_duration_seconds,
         MATCH(transcript) AGAINST(? IN BOOLEAN MODE) as relevance
  FROM voice_notes
  WHERE tenant_id = ? AND MATCH(transcript) AGAINST(? IN BOOLEAN MODE)
  ORDER BY relevance DESC
  LIMIT ? OFFSET ?
");
$query = $_GET['q'];
$stmt->execute([$query, $tenant_id, $query, $limit, $offset]);
```

### Get action items
```php
$stmt = $db->prepare("
  SELECT id, voice_note_id, action_text, due_date, status, confidence, created_at
  FROM voice_note_actions
  WHERE tenant_id = ? AND user_id = ? AND status = 'open'
  ORDER BY due_date ASC
");
$stmt->execute([$tenant_id, $user_id]);
```

---

## TESTING CRITERIA

### Functional Tests

- [ ] Audio file uploads successfully (WAV, MP3, AAC)
- [ ] Transcription completes (self-hosted Whisper)
- [ ] Transcript confidence score calculated
- [ ] Entities extracted (names, prices, dates, actions)
- [ ] Confidence scores appear in review UI
- [ ] Confirm extraction updates CRM (create client, task, etc.)
- [ ] Reject extraction hides entity
- [ ] Search finds voice notes by transcript content
- [ ] Playback syncs to transcript (click snippet → jump to timestamp)
- [ ] Voice notes linked to showing/transaction appear in detail pages
- [ ] Action items auto-create tasks (if confirmed)
- [ ] Returned visitor context preserved (post-showing, client_call, etc.)

### Security Tests

- [ ] SQL injection on search, context_data, all inputs (prepared statements)
- [ ] Cross-tenant data leak (all queries filtered by tenant_id)
- [ ] Unauthorized access (user can only see own voice notes + assigned notes)
- [ ] Audio file path traversal (validate filename)
- [ ] Fair Housing audit on all extracted text (any discriminatory language)

### Performance Tests

- [ ] Audio upload < 5s (for 10 MB file)
- [ ] Transcription < 2x audio duration (self-hosted Whisper on GPU)
- [ ] Extraction < 30s (LLM inference)
- [ ] Search < 500ms (50 notes, full-text)
- [ ] Playback starts < 1s (streaming)

### Data Quality Tests

- [ ] Entity extraction accuracy ≥ 85% (manual spot-check: 100 extracted entities, ≤ 15 errors)
- [ ] Price parsing handles variations ($5M, 5,200,000, "five point two million")
- [ ] Date extraction handles relative dates ("next week", "end of month")
- [ ] Name extraction avoids false positives (e.g., property names confused as person names)

---

## INTEGRATION POINTS

- **showings.php** — Link voice notes to showings, extract feedback
- **transactions.php** — Link to deals, extract critical dates + objections
- **admin.php** — CRM contact creation/update from extractions
- **calendar.php** — Create events from extracted dates
- **daily-action-feed.js** — Show awaiting-confirmation notes, action items
- **cron.php** — Transcription job + extraction job
- **email.php** — Fair Housing audit on extracted text
- **tenants.php** — Multi-tenant isolation

---

## OPERATIONAL NOTES

- **Transcription accuracy varies.** Noisy environments (showing with ambient sound) = lower confidence. Agents should review all extractions before confirming.
- **AI extraction is probabilistic.** Confidence < 70% should trigger agent review. Never auto-apply to CRM without confirmation.
- **Voice notes are audit trail.** Store audio + transcript + all extractions indefinitely (compliance).
- **Fair Housing is non-negotiable.** Any extracted text must pass audit (instruction 06, email.php). If AI detects potential Fair Housing violation, flag and alert agent.
- **Action items are the high-value output.** Post-showing action items (second showing, inspection, appraisal) drive deal momentum. Highlight these in daily feed.
- **Search is a discovery tool.** Agents can find past conversations ("did John mention ocean view?" → search → playback note from 2 months ago).
- **Performance matters on mobile.** Recording + transcription should feel instantaneous. Use background jobs.

---

## EXAMPLE VOICE NOTE FLOW

**Agent scenario:** Kean finishes showing at oceanfront Carmel property. Taps "Record feedback" on mobile. Records 87-second voice note: "Great showing. John and Jane really loved the master suite with ocean view. They're concerned about the roof inspection in the west wing — mentioned they'd want an inspector to look. Said they need to close in 90 days, have financing pre-approved. Want to schedule a second showing for next week. Jane's going to send me architectural plans for the roof repair."

**What happens next:**
1. Audio uploads → transcription starts (15 sec)
2. Transcript returned with 0.94 confidence
3. Extraction job runs:
   - Names: John, Jane (0.98, 0.97 confidence)
   - Feature preference: master suite, ocean view (0.92)
   - Objection: roof inspection concern (0.88)
   - Buyer timeline: 90 days (0.95)
   - Next action: schedule second showing (0.91)
   - Next action: review architectural plans (0.87)
4. Kean reviews extracted entities (takes 20 sec)
5. Kean confirms all 7 extractions
6. System auto-creates:
   - Interaction: "Client call - showing feedback" linked to John + Jane
   - Property note: "Roof concern flagged"
   - Task: "Schedule second showing" (due: 2026-04-05)
   - Task: "Review roof architectural plans" (awaiting Jane's email)
   - Updates buyer profile: timeline = 90 days, financing = pre-approved
7. Next day: Action items appear in Kean's daily action feed
8. Second showing scheduled → another voice note recorded
9. All notes searchable together: "roof" → returns both post-showing notes

---

End of instruction 29.
