Skip to content

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:

  1. New companies: List companies from NEW participants that don't exist in Airtable. Ask user to confirm company creation list.
  2. New contacts: List NEW participants to create as contacts.
  3. Event tags to set: All matched + new contacts need the event participation link.
  4. Event tags to remove: Participants with action="removed" need their event tag unlinked.
  5. Enrichment: Classify hierarchy from job title using hierarchy-rules.json, classify gender.
  6. Country enrichment: Map each participant's CSV country field to a Country table record ID.
  7. Fetch country map: fetch_country_map(base_id, api_key){name_lower: record_id}
  8. For each contact: if country field is empty or missing, skip that contact
  9. Otherwise look up participant.country.lower() in the map
  10. Skip contacts where Country field is already set (from exported data in Step 3)
  11. 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)
  12. Skip companies where Country field is already set
  13. If a participant's country is not found in the map, log a warning and skip that contact
  14. 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 participation is 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 multilineText field, NOT a url type.
  • Company type: This is a linked record to data: Company-type table, 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