# Manage Insights User Skill

This skill automates the creation of Insights users with artist-level (LabelParticipant) permissions using the Altafonte user creation step function. **Supports bulk user creation** for efficient processing of multiple users in a single request.

## Overview

The skill performs the following steps:
1. Query Neo4j prod to get all LabelParticipant UUIDs for the specified GlobalParticipant ID(s) (once per artist, not per user)
2. Generate two CSV files with ALL users:
   - No-email version: One row per user per LabelParticipant with `SendEmail=N`
   - Send-email version: One row per user with `SendEmail=Y`
3. Upload the no-email CSV to S3 (triggers step function)
4. Monitor the step function execution
5. Upload the send-email CSV to S3 (triggers email invites)
6. Monitor the second step function execution
7. Query Neo4j for Identity IDs for ALL users
8. Generate Harness segment CSV (no header) with ALL identity IDs

## Parameters

- `users` (required): JSON array of user objects. Each object must have `first_name`, `last_name`, and `email`.
  - Single user example: `[{"first_name": "Dylan", "last_name": "Bourne", "email": "dylan@example.com"}]`
  - Multiple users example: `[{"first_name": "Dylan", "last_name": "Bourne", "email": "dylan@example.com"}, {"first_name": "Alice", "last_name": "Smith", "email": "alice@example.com"}]`
- `artist_urls` (required): Insights artist page URL(s) or GlobalParticipant ID(s). Can be comma-separated for multiple artists. All users will be granted access to ALL specified artists.
  - Example URL: `https://insights.sonymusic.com/artist/f804f611-da3c-4b60-aa6e-ddfe39e6aeba/`
  - Example ID: `f804f611-da3c-4b60-aa6e-ddfe39e6aeba`
  - Multiple artists: `f804f611-da3c-4b60-aa6e-ddfe39e6aeba,a1b2c3d4-e5f6-7890-abcd-ef1234567890`
- `output_name` (optional): Base name for output CSV files. For bulk requests, use a descriptive name like `Insights-Insanity-Records-2026-04-16`. If not provided, will auto-generate from first user's name.
- `skip_email` (optional): If true, skip the send-email step (default: false)
- `parent_company` (optional): Filter LabelParticipants to only those whose Vendor belongs to a specific parent company. Use the parent company's `name` slug. When omitted, all LabelParticipants are included.

  Known parent company slugs (as of 2026-05-21):
  | Slug | Display Name |
  |------|-------------|
  | `sme` | Sony Music |
  | `theorchard` | The Orchard |

## Usage Examples

### Single User
```
/manage-insights-user \
  --users '[{"first_name": "Dylan", "last_name": "Bourne", "email": "dylan@bournecreatives.com"}]' \
  --artist_urls "https://insights.sonymusic.com/artist/f804f611-da3c-4b60-aa6e-ddfe39e6aeba/"
```

### Multiple Users (Bulk)
```
/manage-insights-user \
  --users '[{"first_name": "Andy", "last_name": "Varley", "email": "av@insanity.com"}, {"first_name": "Alice", "last_name": "Beal", "email": "ab@insanity.com"}, {"first_name": "Kieran", "last_name": "Cullen", "email": "kc@insanity.com"}]' \
  --artist_urls "f804f611-da3c-4b60-aa6e-ddfe39e6aeba,a1b2c3d4-e5f6-7890-1234-567890abcdef" \
  --output_name "Insights-Insanity-Records-2026-04-16"
```

### Create Without Sending Emails
```
/manage-insights-user \
  --users '[{"first_name": "Dylan", "last_name": "Bourne", "email": "dylan@bournecreatives.com"}]' \
  --artist_urls "f804f611-da3c-4b60-aa6e-ddfe39e6aeba" \
  --skip_email true
```

### SME-Only LabelParticipants (Sony Music brands only)
```
/manage-insights-user \
  --users '[{"first_name": "Dylan", "last_name": "Bourne", "email": "dylan@bournecreatives.com"}]' \
  --artist_urls "f804f611-da3c-4b60-aa6e-ddfe39e6aeba" \
  --parent_company "sme" \
  --skip_email true
```

## AWS CLI Wrapper Requirement

This skill requires the `aws-with-awsume` wrapper script located at `~/.local/bin/aws-with-awsume`. This wrapper:
- Automatically sources awsume credentials for the specified AWS profile
- Suppresses terminal escape sequences and noise
- Runs AWS CLI commands with proper temporary credentials

All AWS CLI commands in this skill should be prefixed with `aws-with-awsume prod` instead of just `aws`.

**Example:**
```bash
# Instead of:
aws s3 ls

# Use:
aws-with-awsume prod s3 ls
```

## Implementation Steps

### Step 1: Parse Users Array

Parse the `users` JSON parameter into an array of user objects. Validate each has `first_name`, `last_name`, and `email`.

### Step 2: Extract GlobalParticipant IDs

Parse the `artist_urls` parameter to extract GlobalParticipant UUID(s):
- If it's a URL like `https://insights.sonymusic.com/artist/UUID/`, extract the UUID
- If it's already a UUID, use it as-is
- Split by comma and process each one
- Remove duplicates

### Step 3: Query Neo4j for LabelParticipant UUIDs

For each **unique** GlobalParticipant ID, run the appropriate Cypher query using the `mcp__neo4j-prod__prod-read_neo4j_cypher` tool based on whether `parent_company` is provided.

**Without `parent_company` (all LabelParticipants):**
```cypher
MATCH (gp:GlobalParticipant {id: "<global_participant_id>"})-[:REPRESENTS]->(lp:LabelParticipant)
RETURN lp.uuid
```

**With `parent_company` (filtered by parent company):**
```cypher
MATCH (gp:GlobalParticipant {id: "<global_participant_id>"})-[:REPRESENTS]->(lp:LabelParticipant)
      <-[:HAS_LABEL_PARTICIPANT]-(v:Vendor)<-[:HAS_LABEL]-(cb:CompanyBrand)
      -[:BELONGS_TO]->(pc:ParentCompany {name: "<parent_company>"})
RETURN lp.uuid
```

Collect all LabelParticipant UUIDs across all artists. These will be shared across all users.

**Note:** When `parent_company` is set, warn the user if zero LabelParticipants are returned — this likely means no Vendors for that artist are linked to the specified parent company, rather than a data error.

### Step 4: Generate CSV Files

Determine the filename:
- If `output_name` is provided, use it
- Otherwise, generate: `Insights-<FirstUser.FirstName>-<FirstUser.LastName>-<YYYY-MM-DD>`

**CSV Headers:**
```
FirstName,LastName,Email,TenantUUID,TenantType,IsMasterContact,IsAdmin,Catalog,Marketing,Analytics,Accounting,Collaborators,Banking,SendEmail
```

**No-Email CSV:**
For each user, create a row for EACH LabelParticipant UUID:
```
<first_name>,<last_name>,<email>,<lp_uuid>,label_participant,N,N,N,N,Y,N,N,N,N
```

Example with 2 users and 3 LabelParticipants = 6 rows total:
```
Dylan,Bourne,dylan@example.com,uuid1,label_participant,N,N,N,N,Y,N,N,N,N
Dylan,Bourne,dylan@example.com,uuid2,label_participant,N,N,N,N,Y,N,N,N,N
Dylan,Bourne,dylan@example.com,uuid3,label_participant,N,N,N,N,Y,N,N,N,N
Alice,Smith,alice@example.com,uuid1,label_participant,N,N,N,N,Y,N,N,N,N
Alice,Smith,alice@example.com,uuid2,label_participant,N,N,N,N,Y,N,N,N,N
Alice,Smith,alice@example.com,uuid3,label_participant,N,N,N,N,Y,N,N,N,N
```

**Send-Email CSV:**
Create ONE row per user (use the first LabelParticipant UUID for each):
```
<first_name>,<last_name>,<email>,<first_lp_uuid>,label_participant,N,N,N,N,Y,N,N,N,Y
```

Example with 2 users = 2 rows:
```
Dylan,Bourne,dylan@example.com,uuid1,label_participant,N,N,N,N,Y,N,N,N,Y
Alice,Smith,alice@example.com,uuid1,label_participant,N,N,N,N,Y,N,N,N,Y
```

Save both files to `~/Downloads/`:
- `~/Downloads/<output_name>.csv`
- `~/Downloads/<output_name>-Send-Email.csv`

### Step 5: Upload No-Email CSV to S3

**Important:** Use the `aws-with-awsume` wrapper script (located at `~/.local/bin/aws-with-awsume`) to run AWS CLI commands with awsume credentials. This wrapper handles credential sourcing automatically.

Test credentials:
```bash
aws-with-awsume prod sts get-caller-identity
```

If this fails, the user may need to run `awsume prod` first to authenticate with MFA.

Upload the no-email CSV:
```bash
aws-with-awsume prod s3 cp ~/Downloads/<output_name>.csv s3://prod-altafonte-user-ingestion/
```

### Step 6: Monitor Step Function (No-Email)

Wait 2-3 seconds for the S3 trigger to fire, then get the latest execution:

```bash
sleep 3
EXECUTION_ARN=$(aws-with-awsume prod stepfunctions list-executions \
  --state-machine-arn arn:aws:states:us-east-1:437795906767:stateMachine:prod-workflow-altafonte-user-creation \
  --max-results 1 \
  --query 'executions[0].executionArn' \
  --output text)
```

Poll the execution status every 5 seconds until completion:
```bash
while true; do
    # Get status and extract just the status word (filter out escape sequences)
    STATUS=$(aws-with-awsume prod stepfunctions describe-execution \
        --execution-arn "$EXECUTION_ARN" \
        --query 'status' \
        --output text | grep -o -E '(RUNNING|SUCCEEDED|FAILED|TIMED_OUT|ABORTED)' | tail -1)

    echo "Status: $STATUS"

    if [[ "$STATUS" == "SUCCEEDED" ]]; then
        echo "✓ Step function succeeded"
        break
    elif [[ "$STATUS" == "FAILED" || "$STATUS" == "TIMED_OUT" || "$STATUS" == "ABORTED" ]]; then
        echo "✗ Step function $STATUS"
        exit 1
    fi

    sleep 5
done
```

**Important:** The wrapper script may output terminal escape sequences. Use `grep -o -E` to extract only the status word for reliable comparison.

Possible statuses:
- `RUNNING`: Continue polling
- `SUCCEEDED`: Move to next step
- `FAILED`, `TIMED_OUT`, `ABORTED`: Report error and stop

Provide the user with the console URL:
```
https://us-east-1.console.aws.amazon.com/states/home?region=us-east-1#/v2/executions/details/$EXECUTION_ARN
```

After the step function succeeds, verify the LP count in Neo4j increased as expected for each user:

```cypher
WITH ['email1@example.com', 'email2@example.com'] AS emails
UNWIND emails AS email
MATCH (i:Identity {email: email})-[:HAS_PROFILE]->(p:Profile {profileType: 'InsightsProfile'})
-[:HAS_ACCESS_TO]->(lp:LabelParticipant)
RETURN i.email AS email, count(lp) AS totalLPs
```

Expected count = previous LP count + number of new LPs added. Report a warning if a user's count didn't change.

### Step 7: Upload Send-Email CSV to S3

If `skip_email` is false, upload the send-email CSV:
```bash
aws-with-awsume prod s3 cp ~/Downloads/<output_name>-Send-Email.csv s3://prod-altafonte-user-ingestion/
```

### Step 8: Monitor Step Function (Send-Email)

Repeat the monitoring process from Step 6 using `aws-with-awsume prod` for all AWS CLI commands.

### Step 9: Query Neo4j for Identity IDs

Use the `mcp__neo4j-prod__prod-read_neo4j_cypher` tool to get Identity IDs for **all users**:

Build an array of all user emails and query:

```cypher
WITH ['email1@example.com', 'email2@example.com', 'email3@example.com'] AS emails
UNWIND emails AS email
MATCH (i:Identity)-[:HAS_PROFILE]->(p:Profile {profileType: "InsightsProfile"})
WHERE i.email = email
RETURN i.id
ORDER BY apoc.coll.indexOf(emails, i.email)
```

This will return Identity IDs in the same order as the input emails.

### Step 10: Generate Harness Segment CSV

Create a CSV file with **NO HEADER** containing just the Identity IDs (one per line) for **all users**:

```
<identity_id_1>
<identity_id_2>
<identity_id_3>
...
```

**Important:** The file must NOT have any header row - only Identity IDs, one per line. Adding a header will cause the upload to fail.

Save to: `~/Downloads/<output_name>-Harness.csv`

### Step 11: Manual Harness Segment Upload

**This step requires manual action in the Harness UI.**

1. Open the Sony_Users_Bulk_Import segment definition page:
   ```
   https://app.harness.io/ng/account/cej_iP27SSSgxFiM6TOjAw/all/fme/orgs/TheOrchardFME/projects/Default/org/5b4b5c30-21c9-11ea-a4e7-0a9b522eabbd/ws/5b5212f0-21c9-11ea-a4e7-0a9b522eabbd/segments/a4bc5637-86ee-11ef-bacb-627fbfefd41c/env/5b53e7b0-21c9-11ea-a4e7-0a9b522eabbd/definition
   ```

2. Upload the Harness CSV file:
   - Click the upload button on the segment definition page
   - Select `~/Downloads/<output_name>-Harness.csv`
   - Wait for upload confirmation
   - Verify the Identity ID count increased

3. The segment is used by the `show_sme_data` feature flag to control access to Insights SME features.

**Why Manual?**
The Harness MCP API does not currently support programmatic segment updates. While the API theoretically supports updating segment definitions, retrieving the current segment structure to safely merge new Identity IDs is not possible through available API endpoints. Attempting blind updates risks breaking the production segment.

### Step 12: Summary

Report to the user:
- All generated file paths
- Number of users processed
- Number of LabelParticipants found
- Number of Identity IDs retrieved
- Step function execution results
- Harness segment URL for manual upload
- Next steps

**Example output for bulk request:**
```
✓ Successfully created 6 users with access to 34 LabelParticipants

Generated files:
  • ~/Downloads/Insights-Insanity-Records-2026-04-16.csv (204 rows)
  • ~/Downloads/Insights-Insanity-Records-2026-04-16-Send-Email.csv (6 rows)
  • ~/Downloads/Insights-Insanity-Records-2026-04-16-Harness.csv (6 identity IDs)

Next steps:
  1. Upload Identity IDs to Harness segment:
     https://app.harness.io/ng/account/cej_iP27SSSgxFiM6TOjAw/all/fme/orgs/TheOrchardFME/projects/Default/org/5b4b5c30-21c9-11ea-a4e7-0a9b522eabbd/ws/5b5212f0-21c9-11ea-a4e7-0a9b522eabbd/segments/a4bc5637-86ee-11ef-bacb-627fbfefd41c/env/5b53e7b0-21c9-11ea-a4e7-0a9b522eabbd/definition

     Upload file: ~/Downloads/Insights-Insanity-Records-2026-04-16-Harness.csv

  2. Verify users can access Insights at https://insights.sonymusic.com
```

### Future Enhancement: Automated Harness Upload

**Status:** Investigated but not currently feasible.

**Investigation findings (re-tested 2026-06-09, original 2026-05-06):**
- The `fme_rule_based_segment_definition` resource supports UPDATE operations via Harness MCP
- However, `list` on `fme_rule_based_segment_definition` consistently returns `[]` regardless of params
- `get` is not supported for this resource type; workspace-level `list` on `fme_rule_based_segment` also returns `[]`
- The `update` operation replaces the entire segment definition — there is no PATCH/append
- Since the current state cannot be read, any write would wipe all existing segment members
- The `change_request` execute action has the same limitation (requires passing the full segment object)
- Adding individual users via a `rules` whitelist was also investigated but ruled out for the same reason

**When API access improves:**
This step could be automated by:
1. Retrieving the current segment definition via API
2. Merging new Identity IDs with existing IDs
3. Updating the segment definition programmatically
4. Verifying the update succeeded

**Alternative approach if CSV upload API becomes available:**
The Harness/Split.io platform may provide a CSV upload API endpoint in the future, which would allow programmatic uploads without needing to construct the segment definition object.

## Neo4j Relationship Reference

When querying a user's existing Insights permissions, use the `HAS_ACCESS_TO` relationship from their `InsightsProfile` to `LabelParticipant`:

```cypher
MATCH (i:Identity {email: '<email>'})-[:HAS_PROFILE]->(p:Profile {profileType: 'InsightsProfile'})
-[:HAS_ACCESS_TO]->(lp:LabelParticipant)
RETURN lp.uuid AS lpUuid, lp.name AS lpName
ORDER BY lp.name
```

There is also a `HAS_ADMIN_ACCESS_TO` relationship used for employees and internal admins — this is separate from the `HAS_ACCESS_TO` relationship used for external/label users and should not be confused with it.

To look up an Identity node by email (e.g. to confirm a user exists before proceeding):

```cypher
MATCH (i:Identity)
WHERE i.email IN ['user1@example.com', 'user2@example.com']
RETURN i.email AS email, i.id AS identityId
```

---

## Variant: Copy Permissions from Existing User

Use this variant when the task is to **copy artist permissions from one existing Insights user to another**, rather than creating a new user from artist URLs. This comes up when a user has been set up incorrectly, or a colleague needs the same access as another user.

**Key differences from the standard flow:**
- No artist URLs needed — permissions are derived by diffing two existing users
- `skip_email` is always true (target user already has an account)
- Harness segment step is skipped (target user is already in the segment)

### Step 1: Identify both users

Confirm both the source and target users exist in Neo4j:

```cypher
MATCH (i:Identity)
WHERE i.email IN ['source@example.com', 'target@example.com']
RETURN i.email AS email, i.id AS identityId
```

Also retrieve the target user's profile name (needed for the CSV):

```cypher
MATCH (i:Identity {email: 'target@example.com'})-[:HAS_PROFILE]->(p:Profile {profileType: 'InsightsProfile'})
RETURN p.profileName AS profileName
```

### Step 2: Query both users' existing LabelParticipants

Run in parallel:

```cypher
// Source user
MATCH (i:Identity {email: 'source@example.com'})-[:HAS_PROFILE]->(p:Profile {profileType: 'InsightsProfile'})
-[:HAS_ACCESS_TO]->(lp:LabelParticipant)
RETURN lp.uuid AS lpUuid, lp.name AS lpName
ORDER BY lp.name
```

```cypher
// Target user
MATCH (i:Identity {email: 'target@example.com'})-[:HAS_PROFILE]->(p:Profile {profileType: 'InsightsProfile'})
-[:HAS_ACCESS_TO]->(lp:LabelParticipant)
RETURN lp.uuid AS lpUuid, lp.name AS lpName
ORDER BY lp.name
```

### Step 3: Compute the diff

Find LP UUIDs that are in the source but **not** in the target. These are the only LPs to be added. Report the counts to the user:
- Source LP count
- Target LP count (before)
- Missing LP count (to be added)
- LPs already in target but not source (leave untouched — do not remove)

### Step 4: Generate the no-email CSV

Use the output name format: `Insights-<FirstName>-<LastName>-<JIRA-TICKET>-<YYYY-MM-DD>`

Create one row per **missing** LP only:
```
FirstName,LastName,Email,TenantUUID,TenantType,IsMasterContact,IsAdmin,Catalog,Marketing,Analytics,Accounting,Collaborators,Banking,SendEmail
<first_name>,<last_name>,<target_email>,<missing_lp_uuid>,label_participant,N,N,N,N,Y,N,N,N,N
...
```

Save to `~/Downloads/<output_name>.csv`. No send-email CSV is needed.

### Step 5: Upload to S3 and monitor step function

Same as Steps 5–6 of the standard flow. Use `aws-with-awsume prod` for all AWS CLI commands.

### Step 6: Verify in Neo4j

After the step function succeeds, confirm the target user's LP count is now:
`previous target count + number of missing LPs added`

```cypher
MATCH (i:Identity {email: 'target@example.com'})-[:HAS_PROFILE]->(p:Profile {profileType: 'InsightsProfile'})
-[:HAS_ACCESS_TO]->(lp:LabelParticipant)
RETURN count(lp) AS totalLPs
```

### Step 7: Summary

Report:
- LP counts (source / target before / target after)
- Which artists/LPs were added
- Confirmation that no permissions were removed from the target user
- No Harness segment action required (existing user)

---

## Error Handling

- **No LabelParticipants found**: Report warning and ask user to verify the GlobalParticipant ID
- **AWS credentials expired**: Instruct user to run `awsume prod`
- **Step function failed**: Provide console URL and suggest checking logs
- **Neo4j query error**: Report error and suggest checking Neo4j connectivity

## Important Notes

- This skill ONLY creates users in PROD (not QA)
- The CSV filename should be descriptive for auditing purposes
- Analytics permission is set to `Y`, all other permissions are set to `N`
- TenantType is always `label_participant`
- The user will still need to manually upload the Harness CSV to the segment
