ufi-sync¶
Plugin: ufi
Category: Other
Command: /ufi-sync
/ufi-sync — Process UFI Participant Delta¶
Process new and removed UFI event participants: fuzzy-match against Airtable CRM, review matches, create new contacts/companies, set event tags, and enrich with hierarchy/gender classification.
Arguments¶
--event <slug>— Event slug (e.g.,ufi-apac-2026-bangkok). If omitted, auto-detect active event.--full— Reprocess all participants (not just delta).--dry-run— Preview write plan without Airtable writes.
Workflow¶
When the user invokes /ufi-sync, execute the following steps:
Step 1: Load Event Config¶
# Find the event config
PIPELINE_ROOT="$HOME/Library/CloudStorage/OneDrive-VisiTransGmbH/VisiTrans - Dokumente/3-VisiFair/02-Projekte/F010 - UFI Konferenzen/pipeline-data"
Read $PIPELINE_ROOT/{event-slug}/config.yaml using the Read tool. Parse the YAML to extract:
- event_tag — the exact string used in Airtable UFI event participation
- ufi_event_record_id — Airtable record ID for this event
- airtable_base — Airtable base ID
- matching.auto_approve_threshold — typically "HIGH"
Step 2: Read Delta¶
Read $PIPELINE_ROOT/{event-slug}/delta.csv. This file is produced by ufi-scraper scrape.
If --full flag is set, read current.csv instead and treat all participants as "added".
Report to user: "Found N new, M removed, K changed participants."
Step 3: Export Airtable Data¶
Run the following Python script to export current contacts and companies:
import sys
sys.path.insert(0, '<path-to-plugins/ufi/src>')
from ufi_pipeline.airtable_client import get_api_key, export_contacts, export_companies
api_key = get_api_key()
contacts = export_contacts('<base_id>', api_key)
companies = export_companies('<base_id>', api_key)
print(f"Exported {len(contacts)} contacts, {len(companies)} companies")
Step 4: Fuzzy Match¶
Run the fuzzy matcher:
from ufi_pipeline.fuzzy_matcher import match_participants
from ufi_pipeline.scraper import RawParticipant
# Build participant list from delta.csv rows where action="added"
participants = [RawParticipant(...) for row in delta if row.action == "added"]
results = match_participants(participants, contacts, companies)
Step 5: Review Report¶
Present the match results to the user:
Summary: - HIGH confidence (auto-approved): N contacts - MEDIUM confidence (needs review): M contacts - LOW confidence (needs review): K contacts - NEW (no match): J contacts
For each MEDIUM/LOW match, show:
UFI: "Firstname Lastname" (Company, Country)
↔ Airtable: "Matched Name" — Reason: {match_reason}
→ Accept? [Y/n]
Use AskUserQuestion to let the user accept or reject each MEDIUM/LOW match.
Step 6: Build Write Plan¶
Based on approved matches:
- New companies: List companies from NEW participants that don't exist in Airtable. Ask user to confirm company creation list.
- New contacts: List NEW participants to create as contacts.
- Event tags to set: All matched + new contacts need the event participation link.
- Event tags to remove: Participants with action="removed" need their event tag unlinked.
- Enrichment: Classify hierarchy from job title using
hierarchy-rules.json, classify gender. - Country enrichment: Map each participant's CSV
countryfield to a Country table record ID. - Fetch country map:
fetch_country_map(base_id, api_key)→{name_lower: record_id} - For each contact: if country field is empty or missing, skip that contact
- Otherwise look up
participant.country.lower()in the map - Skip contacts where Country field is already set (from exported data in Step 3)
- For companies: collect all participant countries per company, apply majority rule:
- All same country → HIGH confidence (auto-write)
- Mixed countries → MEDIUM confidence (flag for review)
- Skip companies where Country field is already set
- If a participant's country is not found in the map, log a warning and skip that contact
- Report: "Country enrichment: N contacts, M companies (K HIGH, J MEDIUM), P unmatched"
Step 7: Execute Writes (with approval)¶
Present the complete write plan and ask: "Proceed with Airtable writes? [Y/n]"
If --dry-run, stop here and show what would be written.
If approved, execute in order:
1. Create companies → get record IDs
2. Create contacts (linked to companies) → get record IDs
3. Set event participation tags on all contacts (matched + new)
4. Remove event tags for removed participants
5. Set country on contacts — batch PATCH with {"Country": [country_record_id]} for all matched contacts where country was resolved and not already set
6. Set country on companies (HIGH confidence only) — batch PATCH same format. Present MEDIUM confidence companies for individual review before writing.
7. Set hierarchy and gender on new contacts
Use the Airtable client:
from ufi_pipeline.airtable_client import create_records, update_records, fetch_country_map, CONTACT_TABLE_ID, COMPANY_TABLE_ID, COUNTRY_TABLE_ID
All writes are logged to $PIPELINE_ROOT/{event-slug}/write-log.md.
Step 8: Mark Processed¶
After successful writes, mark the delta as processed:
from ufi_pipeline.diff import mark_processed
from ufi_pipeline.config import load_config
config = load_config(config_path)
mark_processed(config)
Report final summary: "Done. N contacts tagged, M new contacts created, K companies created."
Important Notes¶
- Airtable rate limits: 5 requests/second, 10 records per batch write.
- API key: Always from macOS Keychain, never hardcoded.
- Idempotent: Skip contacts/companies already tagged for this event.
- LinkedRecords:
UFI event participationis a linked record field, not multi-select. To tag a contact, PATCH the field with the event record ID appended to existing linked records. - LinkedIn-URL: This is a
multilineTextfield, NOT aurltype. - Company type: This is a linked record to
data: Company-typetable, NOT a select field.
Python Module Locations¶
All modules are in plugins/ufi/src/ufi_pipeline/:
| Module | Purpose |
|---|---|
config.py |
Event config loader |
scraper.py |
HTML scraper + CSV I/O |
diff.py |
Delta computation + run log |
fuzzy_matcher.py |
Name + company matching |
airtable_client.py |
Airtable API wrapper |
name_parser.py |
Culture-aware name parsing |
Airtable Schema Reference¶
| Table | ID | Key Fields |
|---|---|---|
| Contact | tblGYmwykHGUu9r3k |
Firstname, Lastname, Jobtitle, Company (link), LinkedIn-URL, UFI event participation (link), Country (link) |
| Company | tbl4U3xdsw3wTdWmI |
Company name, Company type (link), Country (link) |
| UFI events | tblo7HLLu6LpnBXVL |
Name, Participants list (URL) |
| Company-type | tblNJvjLBmpIyUcnr |
Company type, sort-field |
| Hierarchy | tblWFUPd06EZG2FeA |
C-Level, Director, Dept manager, Senior, Employee |
| Country | tblDl6qqQio0Zwo0i |
Name |