# Reference: Profile.csv

Enriches the confirmed archive owner's `:UserProfile` with the LinkedIn profile fields. No new nodes, no new edges — `:AdminUser` and `:UserProfile` already exist for any {{productName}} operator and are linked by `[:HAS_PROFILE]` at session start (neo4j-store handles that on session boot, not this skill).

Runs before every other reference because later files display LinkedIn profile fields (headline, summary) on the owner node they search.

## Source

`Profile.csv` — single data row.

## Columns → UserProfile properties

Schema.org camelCase per `platform/plugins/memory/references/schema-base.md`.

| CSV column | Property on `:UserProfile` |
|------------|----------------------------|
| First Name | `givenName` |
| Last Name | `familyName` |
| Maiden Name | `additionalName` (nullable) |
| Address | `address` (nullable) |
| Birth Date | `birthDate` (nullable, ISO 8601) |
| Headline | `headline` |
| Summary | `description` |
| Industry | `industry` (nullable) |
| Zip Code | `postalCode` (nullable) |
| Geo Location | `addressLocality` (nullable) |
| Twitter Handles | `twitterHandles` — array, split on `,` |
| Websites | `websites` — array, split on `,` |
| Instant Messengers | `instantMessengers` — array, split on `,` |

## Anchor

The owner-confirmation flow at the start of the skill resolves the operator's `:UserProfile` elementId — not just the `:AdminUser` userId. That elementId (`$ownerProfileElementId`) is the input to this reference. The `[:HAS_PROFILE]` edge between `:AdminUser` and `:UserProfile` is created by `platform/ui/app/lib/neo4j-store.ts` on every session start; it pre-exists by the time any skill runs.

## Write surface

This reference invokes `mcp__plugin_memory_memory__memory-update` once with the parsed row's properties:

```json
{
  "nodeId": "<elementId of :UserProfile from owner-confirmation>",
  "properties": {
    "givenName": "Joel",
    "familyName": "Smalley",
    "additionalName": null,
    "address": null,
    "birthDate": null,
    "headline": "Founder, Rubytech",
    "description": "Building {{productName}} …",
    "industry": "Software",
    "postalCode": null,
    "addressLocality": "London, UK",
    "twitterHandles": ["@joelsmalley"],
    "websites": ["https://getmaxy.com"],
    "instantMessengers": [],
    "linkedinProfileUpdatedAt": "<ISO 8601 timestamp>",
    "source": "linkedin"
  }
}
```

The `memory-update` tool ignores restricted keys (`embedding`, `accountId`, `createdAt`) and recomputes the embedding from the new property set. No raw Cypher, no `MERGE`, no Bash.

The parser converts:
- Blank cells → `null`.
- `Twitter Handles`, `Websites`, `Instant Messengers` from comma-delimited strings → arrays.
- `Birth Date` to ISO 8601 if present.

## Expected outcome

- Zero new nodes.
- Zero new edges.
- One existing `:UserProfile` enriched with 10–13 new properties.

## Failure modes

| Symptom | Cause | Fix |
|---------|-------|-----|
| Tool error "node not found" | `$ownerProfileElementId` invalid — owner-confirmation flow did not return a UserProfile | Re-run the owner-confirmation flow; verify `:AdminUser`-`HAS_PROFILE`->`:UserProfile` exists for the confirmed userId |
| `up.websites` written as a single string not an array | Parser didn't split on `,` | Fix parser — LinkedIn comma-delimits these fields |
| Restricted property silently ignored | `memory-update` rejects `embedding` / `accountId` / `createdAt` overrides | Expected — those fields are managed by the server |

## Post-import verification (operator-side, read-only)


```cypher
MATCH (au:AdminUser {userId: $ownerUserId})-[:HAS_PROFILE]->(up:UserProfile)
RETURN
  au.name, au.userId,
  up.givenName, up.familyName, up.headline, up.description,
  up.websites, up.linkedinProfileUpdatedAt
```

Exactly one row. If zero, the owner-confirmation flow returned the wrong elementId — investigate before running any subsequent reference.
