SuccessFactors OData Metadata Parser tool
SaferSkills independently audited sf-metadata-parser (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill parses SAP SuccessFactors OData $metadata XML documents and transforms them into user-friendly, organizational-ready outputs. The raw $metadata is a massive XML schema document (often 50,000+ lines) containing entity definitions, field types, navigation properties, associations, and constraints — virtually unreadable for business users, HR admins, and even many consultants. This tool makes it accessible.
In every SAP SuccessFactors implementation, consultants and admins face these recurring pain points:
Raw $metadata XML answers all of these — but no human should have to read 50K lines of XML to find them.
The tool accepts metadata from multiple sources:
User uploads: metadata.xml (or .edmx)
Location: /mnt/user-data/uploads/metadata.xmlUser pastes raw XML into the chat. Extract and save to a working file before processing.
Use the sf-mcp:get_configuration tool to fetch entity metadata directly from a live instance:
sf-mcp:get_configuration(instance, entity, data_center, environment, auth_user_id, auth_password)User uploads two metadata files (e.g., metadata_dev.xml and metadata_prod.xml) for cross-instance comparison.
Use Python's lxml or xml.etree.ElementTree with proper OData namespace handling.
import xml.etree.ElementTree as ET
# OData EDMX namespaces (SuccessFactors uses EDMX + CSDL)
NAMESPACES = {
'edmx': 'http://schemas.microsoft.com/ado/2007/06/edmx',
'edm': 'http://schemas.microsoft.com/ado/2008/09/edm',
'm': 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata',
'd': 'http://schemas.microsoft.com/ado/2007/08/dataservices',
# SF may also use newer namespace versions:
'edm2009': 'http://schemas.microsoft.com/ado/2009/11/edm',
}
def parse_metadata(xml_path):
tree = ET.parse(xml_path)
root = tree.getroot()
# Auto-detect namespace version from root element
ns = detect_namespaces(root)
entities = {}
associations = {}
entity_sets = {}
# Parse EntityTypes
for schema in root.iter(f'{{{ns["edm"]}}}Schema'):
namespace = schema.get('Namespace', '')
for entity_type in schema.findall(f'{ns_prefix}EntityType', ns):
name = entity_type.get('Name')
base_type = entity_type.get('BaseType', '')
entities[name] = {
'name': name,
'namespace': namespace,
'base_type': base_type,
'keys': extract_keys(entity_type, ns),
'properties': extract_properties(entity_type, ns),
'navigation_properties': extract_nav_properties(entity_type, ns),
}
# Parse Associations
for assoc in schema.findall(f'{ns_prefix}Association', ns):
associations[assoc.get('Name')] = extract_association(assoc, ns)
# Parse EntitySets (EntityContainer)
for container in schema.findall(f'{ns_prefix}EntityContainer', ns):
for es in container.findall(f'{ns_prefix}EntitySet', ns):
entity_sets[es.get('Name')] = es.get('EntityType')
return entities, associations, entity_setsFor each entity, extract these critical attributes per field:
| Attribute | Source in XML | Business Meaning |
|---|---|---|
Name | Property/@Name | Technical field name |
Type | Property/@Type | Data type (Edm.String, Edm.DateTime, etc.) |
Nullable | Property/@Nullable | Is the field required? (false = required) |
MaxLength | Property/@MaxLength | Character limit for strings |
Label | sap:label attribute | Human-readable field label |
Creatable | sap:creatable | Can be set on insert? |
Updatable | sap:updatable | Can be modified on update? |
Filterable | sap:filterable | Can be used in $filter? |
Sortable | sap:sortable | Can be used in $orderby? |
Unicode | Property/@Unicode | Unicode support flag |
DefaultValue | Property/@DefaultValue | Default value if any |
SAP-specific attributes (critical for SF):
SAP_ATTRIBUTES = [
'sap:label', # Display label
'sap:creatable', # Insert permission
'sap:updatable', # Update permission
'sap:filterable', # Filter capability
'sap:sortable', # Sort capability
'sap:required-in-filter', # Must be in $filter
'sap:display-format', # Date/time display format
'sap:field-control', # Dynamic field control
'sap:visible', # UI visibility
'sap:semantics', # Semantic meaning (e.g., email, tel)
]def extract_nav_properties(entity_type, ns):
nav_props = []
for nav in entity_type.findall(f'.//NavigationProperty', ns):
nav_props.append({
'name': nav.get('Name'),
'relationship': nav.get('Relationship'),
'from_role': nav.get('FromRole'),
'to_role': nav.get('ToRole'),
'cardinality': determine_cardinality(nav, associations) # 1:1, 1:N, N:1
})
return nav_propsThis is the most commonly requested output. Generate a multi-sheet Excel workbook.
#### Sheet Structure:
| Sheet Name | Contents |
|---|---|
| Summary | Entity list with field count, key fields, nav property count |
| All Fields | Master flat list of every field across all entities |
| {EntityName} | One sheet per entity with full field details (create for top entities or all) |
| Navigation Properties | All relationships between entities |
| Associations | Cardinality and role mappings |
| Enums / ComplexTypes | Picklist values and complex type definitions |
| Comparison | (If two files provided) Side-by-side diff |
#### Excel Formatting Standards:
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# Color scheme (SAP-inspired)
HEADER_FILL = PatternFill('solid', fgColor='1B3A5C') # Dark SAP blue
HEADER_FONT = Font(name='Arial', bold=True, color='FFFFFF', size=11)
SUBHEADER_FILL = PatternFill('solid', fgColor='4A90D9') # Medium blue
KEY_FIELD_FILL = PatternFill('solid', fgColor='FFF3CD') # Yellow highlight for key fields
REQUIRED_FILL = PatternFill('solid', fgColor='F8D7DA') # Light red for required fields
READONLY_FILL = PatternFill('solid', fgColor='E2E3E5') # Gray for read-only fields
NAV_PROP_FILL = PatternFill('solid', fgColor='D4EDDA') # Green for navigation properties
CUSTOM_FIELD_FILL = PatternFill('solid', fgColor='D6EAF8') # Light blue for custom (cust_) fields
ALT_ROW_FILL = PatternFill('solid', fgColor='F8F9FA') # Alternating row color
THIN_BORDER = Border(
left=Side(style='thin', color='CCCCCC'),
right=Side(style='thin', color='CCCCCC'),
top=Side(style='thin', color='CCCCCC'),
bottom=Side(style='thin', color='CCCCCC'),
)#### Column Layout for Entity Field Sheets:
A: Field Name (technical)
B: Label (sap:label — human-readable)
C: Data Type (simplified: String, Integer, DateTime, Boolean, Decimal, etc.)
D: Max Length
E: Required (Yes/No — derived from Nullable=false)
F: Key Field (Yes/No)
G: Creatable (Yes/No)
H: Updatable (Yes/No)
I: Filterable (Yes/No)
J: Sortable (Yes/No)
K: Default Value
L: Description / Notes
M: Custom Field (Yes/No — detected by 'cust_' prefix)#### Summary Sheet Layout:
A: Entity Name
B: Entity Label (if available)
C: Total Fields
D: Key Fields (count)
E: Required Fields (count)
F: Custom Fields (count — cust_ prefix)
G: Navigation Properties (count)
H: Creatable Fields (count)
I: Updatable Fields (count)
J: Category (Employee, Talent, Foundation, Platform, MDF, Custom)Auto-categorization logic:
def categorize_entity(name):
name_lower = name.lower()
if name_lower.startswith('cust_'):
return 'Custom MDF'
elif any(name_lower.startswith(p) for p in ['emp', 'perhire', 'perpersonal', 'peremail', 'perphone']):
return 'Employee Central'
elif any(name_lower.startswith(p) for p in ['goal', 'form', 'competency']):
return 'Talent / Performance'
elif any(name_lower.startswith(p) for p in ['jobrecreq', 'jobreq', 'candidate', 'jobapp']):
return 'Recruiting'
elif any(name_lower.startswith(p) for p in ['fo_', 'picklistoption', 'picklistlabel']):
return 'Foundation Objects'
elif any(name_lower.startswith(p) for p in ['user', 'dynamicgroup', 'rbp']):
return 'Platform / Security'
elif any(name_lower.startswith(p) for p in ['position', 'budget']):
return 'Position Management'
elif any(name_lower.startswith(p) for p in ['timeaccount', 'employeetime', 'timemanagement']):
return 'Time Management'
else:
return 'Other'#### Applying Formatting:
def format_entity_sheet(ws, data_rows):
# Freeze top row
ws.freeze_panes = 'A2'
# Auto-filter on headers
ws.auto_filter.ref = ws.dimensions
# Column widths (optimized for readability)
COLUMN_WIDTHS = {
'A': 35, # Field Name
'B': 35, # Label
'C': 18, # Data Type
'D': 12, # Max Length
'E': 10, # Required
'F': 10, # Key Field
'G': 12, # Creatable
'H': 12, # Updatable
'I': 12, # Filterable
'J': 12, # Sortable
'K': 18, # Default Value
'L': 45, # Description
'M': 14, # Custom Field
}
for col, width in COLUMN_WIDTHS.items():
ws.column_dimensions[col].width = width
# Conditional formatting
for row_idx, row_data in enumerate(data_rows, start=2):
# Highlight key fields
if row_data.get('is_key'):
for col in range(1, 14):
ws.cell(row=row_idx, column=col).fill = KEY_FIELD_FILL
# Highlight required fields
elif row_data.get('required'):
ws.cell(row=row_idx, column=5).fill = REQUIRED_FILL
# Highlight custom fields
elif row_data.get('is_custom'):
ws.cell(row=row_idx, column=1).fill = CUSTOM_FIELD_FILL
# Alternating rows
elif row_idx % 2 == 0:
for col in range(1, 14):
ws.cell(row=row_idx, column=col).fill = ALT_ROW_FILLFor formal documentation deliverables. Use the docx skill for creation.
Document Structure:
For integration with other tools, data governance platforms, or quick lookups:
import csv
def export_flat_csv(entities, output_path):
headers = [
'Entity', 'Field_Name', 'Label', 'Data_Type', 'Max_Length',
'Required', 'Key_Field', 'Creatable', 'Updatable', 'Filterable',
'Sortable', 'Default_Value', 'Custom_Field', 'Category'
]
with open(output_path, 'w', newline='', encoding='utf-8-sig') as f: # BOM for Excel compat
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
for entity_name, entity in entities.items():
category = categorize_entity(entity_name)
for prop in entity['properties']:
writer.writerow({
'Entity': entity_name,
'Field_Name': prop['name'],
'Label': prop.get('label', ''),
'Data_Type': simplify_type(prop['type']),
'Max_Length': prop.get('max_length', ''),
'Required': 'Yes' if not prop.get('nullable', True) else 'No',
'Key_Field': 'Yes' if prop['name'] in entity['keys'] else 'No',
'Creatable': prop.get('creatable', ''),
'Updatable': prop.get('updatable', ''),
'Filterable': prop.get('filterable', ''),
'Sortable': prop.get('sortable', ''),
'Default_Value': prop.get('default_value', ''),
'Custom_Field': 'Yes' if prop['name'].startswith('cust_') else 'No',
'Category': category,
})Generate a single-file HTML with search, filter, and entity navigation.
<!-- Features to include: -->
<!-- 1. Sidebar with entity list (collapsible by category) -->
<!-- 2. Search bar filtering entities and fields -->
<!-- 3. Field table with sortable columns -->
<!-- 4. Color coding matching Excel scheme -->
<!-- 5. Entity relationship links (click nav property → jump to target entity) -->
<!-- 6. Export to CSV button (client-side) -->
<!-- 7. Stats dashboard at top (total entities, fields, custom fields) -->When two metadata files are provided:
def compare_metadata(entities_a, entities_b, label_a='Instance A', label_b='Instance B'):
comparison = {
'only_in_a': [], # Entities only in first instance
'only_in_b': [], # Entities only in second instance
'in_both': [], # Entities in both
'field_differences': {} # Per-entity field-level diffs
}
all_entities = set(entities_a.keys()) | set(entities_b.keys())
for entity_name in sorted(all_entities):
in_a = entity_name in entities_a
in_b = entity_name in entities_b
if in_a and not in_b:
comparison['only_in_a'].append(entity_name)
elif in_b and not in_a:
comparison['only_in_b'].append(entity_name)
else:
comparison['in_both'].append(entity_name)
# Compare fields
fields_a = {p['name'] for p in entities_a[entity_name]['properties']}
fields_b = {p['name'] for p in entities_b[entity_name]['properties']}
only_a = fields_a - fields_b
only_b = fields_b - fields_a
if only_a or only_b:
comparison['field_differences'][entity_name] = {
f'only_in_{label_a}': sorted(only_a),
f'only_in_{label_b}': sorted(only_b),
}
return comparisonAlways simplify OData Edm types to business-friendly names:
TYPE_MAP = {
'Edm.String': 'Text',
'Edm.Int16': 'Integer (16-bit)',
'Edm.Int32': 'Integer',
'Edm.Int64': 'Long Integer',
'Edm.Decimal': 'Decimal',
'Edm.Double': 'Double',
'Edm.Single': 'Float',
'Edm.Boolean': 'Yes/No',
'Edm.DateTime': 'Date & Time',
'Edm.DateTimeOffset': 'Date & Time (UTC)',
'Edm.Time': 'Time',
'Edm.Binary': 'Binary',
'Edm.Byte': 'Byte',
'Edm.Guid': 'GUID',
'Edm.SByte': 'Signed Byte',
}
def simplify_type(edm_type):
if edm_type in TYPE_MAP:
return TYPE_MAP[edm_type]
# Handle complex types and enums
if '.' in edm_type:
return edm_type.split('.')[-1] # Return just the type name
return edm_typeWhen generating the Summary sheet, group entities into these categories for readability:
| Category | Key Entities | Description |
|---|---|---|
| Employee Central | User, EmpEmployment, EmpJob, EmpCompensation, PerPersonal, PerEmail, PerPhone, PerNationalId | Core employee data |
| Position Management | Position, PositionCompetencyMappingEntity, PositionMatrixRelationship | Org structure positions |
| Recruiting | JobRequisition, JobApplication, Candidate, JobOffer | Talent acquisition |
| Onboarding | ONB2Process, ONB2EquipmentActivity | New hire onboarding |
| Performance & Goals | FormTemplate, FormHeader, FormContent, Goal_*, CompetencyEntity | PM & Goal plans |
| Compensation | EmpPayCompRecurring, EmpPayCompNonRecurring, PayScaleGroup | Pay components |
| Time Management | EmployeeTime, TimeAccount, TimeAccountType, TimeType | Leave & attendance |
| Learning | LMSLearningItem, LMSAssignment, LMSCurriculum | LMS entities |
| Foundation Objects | FOCompany, FODepartment, FODivision, FOLocation, FOJobCode, FOCostCenter, FOPayGrade | Org building blocks |
| Platform / Security | RBPRole, DynamicGroup, Permission, Todo | Security & workflow |
| MDF / Custom | cust_* | Customer-created generic objects |
1. User asks about a specific entity (e.g., "What fields are on EmpJob?")
2. If metadata file is uploaded → Parse XML, extract that entity
3. If SF MCP tools available → Call get_configuration(entity="EmpJob")
4. Display field list in a clean table (in chat or Excel)1. User uploads $metadata XML or requests full parse
2. Parse entire metadata document
3. Generate multi-sheet Excel workbook following formatting standards above
4. Save to /mnt/user-data/outputs/SF_Data_Dictionary.xlsx
5. Present to user with summary stats1. User uploads two metadata files
2. Parse both files independently
3. Run comparison logic
4. Generate comparison Excel with:
- Summary sheet (counts, match %)
- Entities only in Instance A
- Entities only in Instance B
- Field-level differences for shared entities
5. Color-code: Green = match, Red = only in A, Blue = only in BSF instances may use different OData/EDMX namespace versions. Always auto-detect:
def detect_namespaces(root):
tag = root.tag
# Extract namespace from root tag
if '{' in tag:
edmx_ns = tag.split('}')[0].strip('{')
# Look for Schema namespace in children
for child in root.iter():
if 'Schema' in child.tag:
edm_ns = child.tag.split('}')[0].strip('{')
break
return {'edmx': edmx_ns, 'edm': edm_ns}Some SF instances have 500+ entities and 10,000+ fields. Strategies:
iterparse for files > 50MBNot all instances expose sap:label, sap:creatable, etc. Gracefully default:
def get_sap_attr(element, attr_name, default='N/A'):
return element.get(f'{{http://www.sap.com/Protocols/SAPData}}{attr_name}', default)Always flag custom fields with visual indicators. These are the fields customers added via MDF and are critical for:
xml.etree.ElementTree for XML parsingpip install lxml --break-system-packages)/mnt/skills/public/docx/SKILL.md)/mnt/skills/public/xlsx/SKILL.md)Edm.DateTimeOffset means. Show "Date & Time (UTC)".~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.