AgentStack
SKILL verified MIT Self-run

Sf Metadata Parser

skill-shaktibarath-sf-metadata-parser-sf-metadata-parser · by shaktibarath

Use this skill whenever the user wants to parse, analyze, document, or export SAP SuccessFactors OData metadata ($metadata XML). Triggers include: any mention of 'metadata', '$metadata', 'OData entities', 'entity fields', 'SF metadata', 'SuccessFactors schema', 'data dictionary', or requests to understand, compare, or export the structure of SF entities. Also use when generating field-level docum…

No reviews yet
0 installs
2 views
0.0% view→install

Install

$ agentstack add skill-shaktibarath-sf-metadata-parser-sf-metadata-parser

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Sf Metadata Parser? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

SAP SuccessFactors OData Metadata Parser

Overview

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.

Why This Tool Exists

In every SAP SuccessFactors implementation, consultants and admins face these recurring pain points:

  1. Field Discovery — "What fields exist on EmpJob? Which are required? What's the max length?"
  2. Data Mapping — "Map SF fields to our payroll system — we need entity, field, type, and constraints in Excel."
  3. Configuration Documentation — "Generate a data dictionary for our instance for audit/compliance."
  4. Instance Comparison — "Did someone add custom fields in production that aren't in our test environment?"
  5. Integration Planning — "Which navigation properties connect User to EmpEmployment? What's the cardinality?"

Raw $metadata XML answers all of these — but no human should have to read 50K lines of XML to find them.


Input Sources

The tool accepts metadata from multiple sources:

1. Uploaded $metadata XML File

User uploads: metadata.xml (or .edmx)
Location: /mnt/user-data/uploads/metadata.xml

2. Pasted XML Content

User pastes raw XML into the chat. Extract and save to a working file before processing.

3. Live API Fetch (if SF MCP tools are available)

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)

4. Multiple Files for Comparison

User uploads two metadata files (e.g., metadata_dev.xml and metadata_prod.xml) for cross-instance comparison.


Core Parsing Logic

Step 1: Parse the $metadata XML

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_sets

Step 2: Extract Field-Level Details

For 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)
]

Step 3: Extract Navigation Properties & Relationships

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_props

Output Formats

Output 1: Excel Data Dictionary (Primary Deliverable)

This 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_FILL

Output 2: Word Document (Data Dictionary Report)

For formal documentation deliverables. Use the docx skill for creation.

Document Structure:

  1. Cover Page — Title, instance name, generation date, author
  2. Table of Contents
  3. Executive Summary — Total entities, fields, custom objects count
  4. Entity Catalog — Grouped by category (EC, Talent, Recruiting, etc.)
  5. Entity Detail Pages — One section per entity with field table
  6. Navigation Property Map — Entity relationship documentation
  7. Appendix: Data Type Reference — Edm type to business type mapping
  8. Appendix: SAP Attribute Legend — What each sap:* attribute means

Output 3: CSV / Flat File Export

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,
                })

Output 4: HTML Interactive Data Dictionary

Generate a single-file HTML with search, filter, and entity navigation.

Output 5: Instance Comparison Report

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 comparison

Data Type Mapping Reference

Always 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

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [shaktibarath](https://github.com/shaktibarath)
- **Source:** [shaktibarath/sf-metadata-parser](https://github.com/shaktibarath/sf-metadata-parser)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.