Interactive customer onboarding -- add customers to the registry from Salesforce data with SE-provided overlays. Run '/customer-setup CustomerName' to add one customer, or '/customer-setup --all' to bulk-import from Salesforce. Trigger on 'add customer', 'onboard customer', 'set up customer', 'populate registry', 'customer-setup'.
Interactive onboarding skill for adding customers to templates/customers.yaml. Queries Salesforce for account-level data and prompts the SE for overlay fields (Slack channels, cadence schedule, Asana project GID) that don't exist in Salesforce.
Partial entries are acceptable -- use PLACEHOLDER markers for any field the SE skips or that cannot be resolved from Salesforce. Consumer skills (customer-snapshot, cadence-prep, jira-check, 3p-update) all handle PLACEHOLDER gracefully.
~/.fe-skills/.env (run /salesforce-setup first). Optional -- if SFDC is unavailable, the skill falls back to manual entry for all fields.ATLASSIAN_EMAIL, ATLASSIAN_TOKEN.SLACK_TOKEN, SLACK_COOKIE.| Field | Source | Required for |
|---|
| name | SE input | All skills |
| jira_customer | SE confirms from SFDC name | jira-check, customer-snapshot, cadence-prep |
| jira_customer_variants | SE input | jira-check |
| sfdc_account_id | SFDC Account.Id | bigquery, customer-snapshot |
| slack_channels | SE input (Slack search) | customer-snapshot, cadence-prep, 3p-update |
| action_tracker | SE input ("asana" or "sheets") | 3p-update, customer-snapshot, nag, ghosted |
| action_tracker_id | SE input (Asana project GID or Sheets URL) | 3p-update, customer-snapshot |
| raid_tracker | SE input ("asana" or "sheets") | raid, cadence-prep, maction |
| raid_tracker_id | SE input (Asana RAID project GID or Sheets URL) | raid, cadence-prep |
| portfolio_id | SE input (Asana portfolio GID) | asana portfolio views |
| deployment_type | SFDC or SE input | cadence-prep |
| cadence | SE input | cadence-prep |
| contacts | SE-managed | cadence-prep |
/customer-setup <CustomerName>)Extract the customer name from the user's input. This becomes the name field in customers.yaml.
Read templates/customers.yaml and check if the customer already exists:
First, discover available fields to understand the org's schema:
uv run --project .claude/skills/salesforce python .claude/skills/salesforce/scripts/accounts.py describe --filter "arr" --pretty
Then search for the customer by name:
uv run --project .claude/skills/salesforce python -c "
import sys
sys.path.insert(0, '.claude/skills/salesforce/scripts')
from sfdc_client import get_client
sf = get_client()
result = sf.query(\"SELECT Id, Name, Type FROM Account WHERE Name LIKE '%<CustomerName>%' AND Type = 'Customer'\")
for r in result['records']:
print(f'{r[\"Name\"]} | {r[\"Id\"]}')
"
If a matching account is found, fetch full details including account team:
uv run --project .claude/skills/salesforce python .claude/skills/salesforce/scripts/accounts.py account-detail --account-id <ID> --pretty
Note: AccountTeamMember is NOT available in W&B's SFDC org. Account team members are stored as reference fields directly on the Account object. Resolve them separately:
uv run --project .claude/skills/salesforce python -c "
import sys
sys.path.insert(0, '.claude/skills/salesforce/scripts')
from sfdc_client import get_client
sf = get_client()
r = sf.query(\"SELECT OwnerId, Post_Sales_SMLE__c, Solutions_Architect__c FROM Account WHERE Id = '<ACCOUNT_ID>'\")['records'][0]
ids = [v for v in [r.get('OwnerId'), r.get('Post_Sales_SMLE__c'), r.get('Solutions_Architect__c')] if v]
users = sf.query(\"SELECT Id, Name, Email FROM User WHERE Id IN ('{}')\" .format(\"','\".join(ids)))
role_map = {r.get('OwnerId'): 'Account Owner', r.get('Post_Sales_SMLE__c'): 'Post-Sales AISE', r.get('Solutions_Architect__c'): 'Solutions Architect'}
for u in users['records']:
print(f'{role_map.get(u[\"Id\"],\"?\")} | {u[\"Name\"]} | {u[\"Email\"]}')
"
Then look up each team member's Slack user ID. Do NOT use users.py search-name — it paginates the entire workspace and hits rate limits on large orgs like CoreWeave. Use one of these approaches instead (in priority order):
Approach 1: Message search by handle (fastest, most reliable)
uv run --project .claude/skills/slack python .claude/skills/slack/scripts/search.py --query "from:@<slack-handle>" --count 1
Extract user field from the first match. Derive handle from email prefix (e.g., [email protected] → try @astevenson, @allan.stevenson; [email protected] → try @fjorge, @flamarion.jorge).
Approach 2: Channel members — Get members of a small channel the person is known to be in. Match by display name. Good when you don't know their handle.
Approach 3: Profile URL — Ask the SE to paste a Slack profile link (e.g., https://coreweave.enterprise.slack.com/team/U08R4K46ER1). The user ID is in the URL. Zero API calls.
Present SFDC data to SE for confirmation before writing.
Auto-fill from SFDC (W&B field mapping discovered 2026-03-24):
sfdc_account_id -- Account.Id (18-char, also used as BigQuery account ID)deployment_type -- Opportunity_Deployment_Types__c (map to saas/dedicated-cloud/server)Note: Business data fields (arr, contract_end, renewal_date, cs_tier, subscription_plan, account_team) are pulled from SFDC at runtime via the /salesforce skill -- they are NOT stored in customers.yaml. The routing table only stores pointers to external systems.
If SFDC unavailable (credentials not configured, auth fails, or no API permissions): Prompt the SE to enter each field manually. This is the D-05 fallback -- the schema is SFDC-ready even when populated by hand.
The SFDC Account.Name (e.g., "G-Research Ltd") often differs from the Jira Customer field value (e.g., "GResearch"). The Jira Customer field uses unpredictable formats — hyphenated, parenthesized, abbreviated. Never guess the Jira name from the SFDC name.
Discovery approach (in priority order):
uv run --project .claude/skills/jira python -c "
import sys; sys.path.insert(0, '.claude/skills/jira/scripts')
from jira_client import get_client; jira = get_client()
result = jira.search_issues('filter=<FILTER_ID>', maxResults=5, fields='customfield_10083')
for i in result:
raw = i.raw['fields'].get('customfield_10083', [])
for item in raw:
val = item.get('value', str(item)) if isinstance(item, dict) else str(item)
print(f'{i.key} | Customer: {val}')
"
uv run --project .claude/skills/jira python -c "
import sys; sys.path.insert(0, '.claude/skills/jira/scripts')
from jira_client import get_client; jira = get_client()
result = jira.search_issues('project = WB AND \"Customer\" is not EMPTY', maxResults=200, fields='customfield_10083')
vals = set()
for i in result:
for item in i.raw['fields'].get('customfield_10083', []):
val = item.get('value', str(item)) if isinstance(item, dict) else str(item)
if '<keyword>' in val.lower():
vals.add(val)
for v in sorted(vals): print(v)
"
uv run --project .claude/skills/jira python .claude/skills/jira/scripts/issues.py list --customer "<jira_customer>" --max-results 1 --pretty
CRITICAL: If a search returns 0 results, the search term is wrong — NOT that the customer has no tickets. Try broader queries before concluding. Examples of format mismatches:
MOD-UK-(Mike-M/James-C-org)GResearch (happens to match)Isomorphic (shorter)jira_customer_variants — alternative spellings the SE has seen in Jira.These fields don't exist in Salesforce and require SE input:
Slack channels:
Ask the SE for channel name(s) (e.g., "ext-gresearch", "supp-gresearch"). Look up the channel ID:
uv run --project .claude/skills/slack python .claude/skills/slack/scripts/channels.py search "<channel-name>"
For each channel, capture:
id -- Slack channel ID (from search results)name -- Channel name (e.g., ext-gresearch)type -- external | internal | support (infer from prefix: ext-* = external, supp-* = support, otherwise ask)Cadence schedule:
Ask the SE:
type -- weekly | biweekly | monthly | qbrday -- Day of week (e.g., Tuesday)time -- Time with timezone (e.g., "10:00 AM PT")Asana project GID:
Ask if the customer has an Asana project for SE action tracking. If yes, look up existing projects:
uv run --project .claude/skills/asana python .claude/skills/asana/scripts/query.py projects --pretty
If the project exists, use its GID. If not, offer to create one:
uv run --project .claude/skills/asana python .claude/skills/asana/scripts/mutate.py setup-project --name "<CustomerName>" --pretty
Deployment type:
If not already resolved from SFDC in Step 3, ask the SE:
saas -- Multi-tenant SaaS (app.wandb.ai)dedicated-cloud -- Customer-specific cloud instance, managed by W&Bserver -- Self-hosted by customer (on-prem or customer cloud)Contacts:
Build contacts list from SE input (account team data is pulled from SFDC at runtime, not stored in customers.yaml). Each contact has:
name -- Contact nameorg -- Organization (e.g., W&B, G-Research)role -- Contact roleCreate the customer's Confluence page tree under the W&B EMEA Account Management page (ID: 1393229828) in the FE space. This gives the customer a place for meeting notes, ongoing issues, and feedback.
Create customer parent page:
uv run --project .claude/skills/confluence python .claude/skills/confluence/scripts/pages.py create \
--title "<CustomerName>" --body "<p><CustomerName> account page.</p>" \
--parent-id 1393229828 --space fe --pretty
Create three child pages under the new customer page:
# Meeting Notes
uv run --project .claude/skills/confluence python .claude/skills/confluence/scripts/pages.py create \
--title "<CustomerName> - Meeting Notes" \
--body '<p>This page is for all individual meeting notes and summaries.</p><p>Rules:</p><ul><li><p>Create one collapsible/foldable bullet per meeting.</p></li><li><p>Bullet title format MUST be: "YYYY-MM-DD — <Short Title> (<Participants>)"</p></li><li><p>Each meeting bullet MUST include a link to the Gong call, Granola notes, and/or Slack thread(s).</p></li><li><p>Each meeting bullet MUST include context (summary, notes, action items).</p></li></ul>' \
--parent-id <customer_page_id> --space fe --pretty
# Ongoing Issues & Requests (with live Jira macros)
uv run --project .claude/skills/confluence python .claude/skills/confluence/scripts/pages.py create \
--title "<CustomerName> - Ongoing Issues & Requests" \
--body '<h2>Open Bugs</h2><ac:structured-macro ac:name="jira" ac:schema-version="1"><ac:parameter ac:name="columns">key,summary,status,priority,created,updated</ac:parameter><ac:parameter ac:name="maximumIssues">50</ac:parameter><ac:parameter ac:name="jqlQuery">project = "WB" AND "Customer (WB)" IN ("<jira_customer>") AND issuetype = Bug AND status NOT IN ("Done", "Archive", "Merged") ORDER BY priority DESC, updated DESC</ac:parameter></ac:structured-macro><h2>Feature Requests</h2><ac:structured-macro ac:name="jira" ac:schema-version="1"><ac:parameter ac:name="columns">key,summary,status,priority,created,updated</ac:parameter><ac:parameter ac:name="maximumIssues">50</ac:parameter><ac:parameter ac:name="jqlQuery">project = "WB" AND "Customer (WB)" IN ("<jira_customer>") AND issuetype = "Feature Request" AND status NOT IN ("Done", "Archive", "Merged") ORDER BY priority DESC, updated DESC</ac:parameter></ac:structured-macro><h2>All Open Issues</h2><ac:structured-macro ac:name="jira" ac:schema-version="1"><ac:parameter ac:name="columns">key,summary,issuetype,status,priority,created,updated</ac:parameter><ac:parameter ac:name="maximumIssues">100</ac:parameter><ac:parameter ac:name="jqlQuery">project = "WB" AND "Customer (WB)" IN ("<jira_customer>") AND status NOT IN ("Done", "Archive", "Merged") ORDER BY priority DESC, updated DESC</ac:parameter></ac:structured-macro>' \
--parent-id <customer_page_id> --space fe --pretty
# Feedback
uv run --project .claude/skills/confluence python .claude/skills/confluence/scripts/pages.py create \
--title "<CustomerName> - Feedback" \
--body '<p>This page tracks customer feedback — NPS scores, survey responses, and qualitative sentiment signals from the customer'\''s perspective. Only include feedback originating from the customer, not our internal assessments.</p><h1>OPEN</h1><p>(feedback items actively being addressed)</p><h1>BACKLOG</h1><p>(acknowledged but not actioned)</p><h1>COMPLETED</h1><p>(resolved)</p>' \
--parent-id <customer_page_id> --space fe --pretty
Record page IDs in the customer's entry as confluence_pages:
confluence_pages:
meeting_notes: "<meeting_notes_page_id>"
ongoing_issues: "<ongoing_issues_page_id>"
feedback: "<feedback_page_id>"
Note: Include all jira_customer_variants in the Jira macro IN clause for the Ongoing Issues page. Use status NOT IN ("Done", "Archive", "Merged") — do NOT use status names with parentheses (they break the Confluence Jira macro parser).
If Confluence credentials are not configured: Skip this step and note that Confluence pages can be created later by re-running /customer-setup.
Use Claude's Read tool to read the current templates/customers.yaml, then use Claude's Edit tool to add or update the customer entry.
For new customers: Append a new entry at the end of the customers: list, following the same YAML structure as the GResearch entry.
For existing customers: Update only the fields that changed (PLACEHOLDER -> real value, or SE-confirmed overwrites). Preserve all existing entries, comments, and formatting.
Use PLACEHOLDER for any field the SE skips or that could not be resolved.
templates/customers.yaml and show it to the SE for final confirmation.uv run --project .claude/skills/jira python .claude/skills/jira/scripts/issues.py list --customer "<jira_customer>" --max-results 3 --pretty
<name> has been added to the registry. Run /customer-snapshot <name> or /jira-check to verify full integration."/customer-setup --all)AccountTeamMember is NOT available in W&B's org. Instead, find accounts where the current user is the Post-Sales AISE:
uv run --project .claude/skills/salesforce python -c "
import sys
sys.path.insert(0, '.claude/skills/salesforce/scripts')
from sfdc_client import get_client
import requests
sf = get_client()
# Get current user ID
resp = requests.get(f'https://{sf.sf_instance}/services/oauth2/userinfo', headers={'Authorization': f'Bearer {sf.session_id}'}, timeout=10)
user_id = resp.json()['user_id']
# Find accounts where user is AISE
result = sf.query_all(f\"SELECT Id, Name, Type FROM Account WHERE Post_Sales_SMLE__c = '{user_id}' AND Type = 'Customer'\")
for r in result['records']:
print(f'{r[\"Name\"]} | {r[\"Id\"]}')
"
Show the SE the list of accounts found in SFDC. Ask which accounts to onboard -- not all accounts may be actively managed (per D-01, only actively managed accounts belong in the registry).
For each account, show: Account Name and Account ID.
For each selected account, run the per-customer flow (Mode 1, Steps 3-7). The SFDC data is already available from the batch query, so Step 3 (Query Salesforce) can use the cached data rather than re-querying.
After all selected accounts are processed, show a summary:
/customer-setup <name> again)This skill is idempotent -- designed to be run repeatedly without causing data loss:
/customer-setup GResearch when GResearch already exists updates PLACEHOLDER fields only. Existing non-PLACEHOLDER values are preserved unless the SE explicitly chooses to overwrite./customer-setup --all skips accounts already in the registry (unless the SE opts to refresh them)./salesforce-setupjira_customer_variants/slack-setupaccounts.py describe --filter "<field>" to check available fields/salesforce-setup -- Configure SFDC credentials (prerequisite)/salesforce -- Direct SFDC queries (account details, team members, field discovery)/credential-status -- Check health of all configured credentials/jira-check -- Verify Jira integration works for registered customers/customer-snapshot -- Generate customer intelligence dashboard/cadence-prep -- Prepare cadence meeting materials