.claude/skills/extracting-filemaker-business-logic/SKILL.md
Use when analyzing FileMaker DDR to extract calculations, custom functions, and business logic for PostgreSQL import processes or maintenance scripts - focuses on understanding and adapting FileMaker logic rather than direct schema migration
npx skillsauth add ajbcoding/claude-skill-eval extracting-filemaker-business-logicInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
FileMaker DDR reports contain calculation fields, custom functions, and business logic embedded in scripts. This skill helps you extract, understand, and adapt that logic for PostgreSQL implementations—typically for import processes, data transformations, and maintenance scripts.
Key principle: PostgreSQL implementations will often be more efficient than direct translations. Use FileMaker logic to understand what the business rules are, then implement them idiomatically in PostgreSQL.
Use this skill when:
Do NOT use for:
Detailed DDR: {ProjectName}_ddr/{DatabaseName}.html (5-20MB file)
Example from user's project:
/Users/anthonybyrnes/PycharmProjects/Python419/AugustServer_ddr/AugustServer.htmlIn DDR HTML, calculation fields appear in field definitions:
Field Name: wtu_calculation
Type: Calculation
Result Type: Number
Formula:
contacthours * units * csfactor
XPath pattern (adjust based on actual DDR structure):
from lxml import etree
tree = etree.parse("AugustServer_ddr/AugustServer.html", etree.HTMLParser())
# Find calculation fields
calc_fields = tree.xpath('//td[contains(text(), "Calculation")]/parent::tr')
for field_row in calc_fields:
field_name = field_row.xpath('./td[1]/text()')[0]
formula = field_row.xpath('./following-sibling::tr//text()')
Important: Many "calculated" values are actually set by FileMaker scripts, not calculation fields. Scripts often contain the most complex business logic.
Look for scripts that:
Example: The WTU field may appear as a Number field (not Calculation), but scripts like "419F - Loop WTU Contact Hours" contain the actual calculation logic.
Script: 419F - Loop WTU Contact Hours - c courses
Steps:
If [Units ≠ "-"]
Set Field [ClassInstance::wtu; ACCU * Workload_Weight_Factor]
Set Field [ClassInstance::contactHours; ACCU * contact_hours_per_unit]
End If
Extraction tip: Search DDR for field names (e.g., "wtu") to find all scripts that reference them.
Custom functions section (anchor: #valCustomFunctionsSectionAnchor_):
Function Name: GenerateUUID
Parameters: none
Formula:
Upper(Get(UUID))
Look for patterns in custom function names:
CF_, Calc_)Validate_, Format_, Calculate_)| FileMaker | PostgreSQL | Notes |
|-----------|------------|-------|
| Get(UUID) | gen_random_uuid() or uuid_generate_v4() | FileMaker UUIDs are uppercase |
| Upper(text) | UPPER(text) | Direct equivalent |
| Left(text, n) | LEFT(text, n) | Direct equivalent |
| Right(text, n) | RIGHT(text, n) | Direct equivalent |
| Position(search, text, start, occurrence) | POSITION(search IN text) | PostgreSQL simpler, use SUBSTRING for start/occurrence |
| Substitute(text, search, replace) | REPLACE(text, search, replace) | Direct equivalent |
| Let([var1 = value; var2 = value]; expression) | WITH vars AS (...) or function variables | FileMaker's scoped variables |
| Case(test1; result1; test2; result2; default) | CASE WHEN test1 THEN result1 WHEN test2 THEN result2 ELSE default END | Similar structure |
| If(test; trueResult; falseResult) | CASE WHEN test THEN trueResult ELSE falseResult END | Or use IF in PL/pgSQL |
| GetField(fieldName) | Dynamic SQL or CASE statement | FileMaker allows dynamic field references |
| Count(relationship::field) | SELECT COUNT(*) FROM related_table WHERE... | Relationship counts become subqueries |
| Sum(relationship::field) | SELECT SUM(field) FROM related_table WHERE... | Aggregate from related table |
& (concatenation) → || in PostgreSQL≠ or != → <> or != in PostgreSQLand, or, not → AND, OR, NOT in PostgreSQL¶ (paragraph return) → E'\n' in PostgreSQLScan DDR for (in priority order):
Critical: Check scripts FIRST. Many fields appear as "Number" or "Text" but are actually calculated by scripts.
For each calculation field:
**Field**: ClassInstance.wtu
**Purpose**: Calculate weighted teaching units for workload reporting
**Formula**: `contacthours * units * csfactor`
**Dependencies**: contacthours, units, csfactor fields
**Used by**: WTU reports, faculty workload calculations
**Implementation**: PostgreSQL VIEW or calculated during import
FileMaker approach (calculation field):
// FileMaker calculation field
Case(
enrollment_total = 0; "Empty";
enrollment_total < class_capacity * 0.5; "Low";
enrollment_total >= class_capacity; "Full";
"Adequate"
)
PostgreSQL approach (CASE expression in VIEW):
CREATE OR REPLACE VIEW class_status AS
SELECT
id,
class_nbr,
CASE
WHEN enrollment_total = 0 THEN 'Empty'
WHEN enrollment_total < class_capacity * 0.5 THEN 'Low'
WHEN enrollment_total >= class_capacity THEN 'Full'
ELSE 'Adequate'
END AS status
FROM classinstance;
Or as import script logic (Python):
def calculate_class_status(enrollment_total, class_capacity):
"""Adapted from FileMaker ClassInstance.status calculation"""
if enrollment_total == 0:
return 'Empty'
elif enrollment_total < class_capacity * 0.5:
return 'Low'
elif enrollment_total >= class_capacity:
return 'Full'
else:
return 'Adequate'
# Use during import
cursor.execute("""
UPDATE classinstance
SET enrollment_status = %s
WHERE id = %s
""", (calculate_class_status(row['enrollment_total'], row['class_capacity']), row['id']))
FileMaker custom function:
Function: GenerateUUID
Parameters: none
Code:
Upper(Get(UUID))
PostgreSQL equivalent (in existing codebase pattern):
def generate_uuid():
"""Generate uppercase UUID matching FileMaker format"""
return str(uuid.uuid4()).upper()
Reference: program_catalog_parser.py:116-120
FileMaker:
// Count related records
Count(ClassAssign::id)
// Sum from related table
Sum(Enrollment::units)
PostgreSQL (import script):
# Calculate during import
cursor.execute("""
SELECT COUNT(*)
FROM classassign
WHERE id_classinstance = %s
""", (classinstance_id,))
assign_count = cursor.fetchone()[0]
# Or as a VIEW
CREATE VIEW classinstance_summary AS
SELECT
ci.id,
COUNT(ca.id) as assignment_count,
SUM(e.units) as total_enrollment_units
FROM classinstance ci
LEFT JOIN classassign ca ON ca.id_classinstance = ci.id
LEFT JOIN enrollment e ON e.id_classinstance = ci.id
GROUP BY ci.id;
FileMaker:
Let([
base = contacthours * units;
factor = Case(
component = "LAB"; 1.5;
component = "LEC"; 1.0;
1.0
)
];
base * factor
)
PostgreSQL function:
CREATE OR REPLACE FUNCTION calculate_wtu(
contact_hours NUMERIC,
units NUMERIC,
component_type TEXT
) RETURNS NUMERIC AS $$
DECLARE
base NUMERIC;
factor NUMERIC;
BEGIN
base := contact_hours * units;
factor := CASE component_type
WHEN 'LAB' THEN 1.5
WHEN 'LEC' THEN 1.0
ELSE 1.0
END;
RETURN base * factor;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
FileMaker:
// Extract course code from title
Let([
spacePos = Position(" "; course_title; 1; 1)
];
Left(course_title; spacePos - 1)
)
PostgreSQL (import script):
def extract_course_code(course_title):
"""Extract course code from title (FileMaker logic)"""
space_pos = course_title.find(' ')
if space_pos > 0:
return course_title[:space_pos]
return course_title
# Or SQL function
CREATE OR REPLACE FUNCTION extract_course_code(course_title TEXT)
RETURNS TEXT AS $$
BEGIN
RETURN SPLIT_PART(course_title, ' ', 1);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
FileMaker scripts often contain:
Focus on extracting:
When building import scripts:
# Reference: program_catalog_parser.py patterns
def process_catalog_entry(row, catalog_year):
"""
Process catalog entry with business logic adapted from FileMaker.
FileMaker calculation: catalog_year format "2024-2025"
FileMaker custom function: ConvertToAY("2024-2025") → "24-25"
"""
# Adapt FileMaker's year conversion logic
ay_short = catalog_year[2:4] + '-' + catalog_year[7:9]
# Get AY record (FileMaker relationship equivalent)
cursor.execute("SELECT id FROM ay WHERE ay = %s", (ay_short,))
id_ay = cursor.fetchone()[0]
# Apply business rule (from FileMaker calculation)
if row['total_units'] is None:
# FileMaker: If(IsEmpty(total_units); Calculate_Default_Units; total_units)
total_units = calculate_default_units(row)
else:
total_units = row['total_units']
return {
'id_ay': id_ay,
'total_units': total_units,
# ... other fields
}
Mistake 1: Literal Translation
Mistake 2: Ignoring Context
Mistake 3: Missing Dependencies
Mistake 4: Over-Engineering
Mistake 5: Skipping Documentation
Mistake 6: Only Checking Calculation Fields
| Rationalization | Reality | |-----------------|---------| | "The field is type Number, so it's not calculated" | FileMaker scripts often calculate and set Number/Text fields. Check scripts that reference the field. | | "I'll just look at calculation fields" | Most complex logic is in scripts (SetField steps), not calculation field types. Scripts are the PRIMARY source. | | "This is too complex to extract, I'll rebuild from scratch" | You'll miss critical business rules. Extract the logic first, then refactor for PostgreSQL. | | "I can translate this literally to PostgreSQL" | FileMaker idioms differ from PostgreSQL. Understand the business rule, then implement idiomatically. | | "I don't need to document this, the code is self-explanatory" | Business context gets lost. Document WHY the calculation exists, not just WHAT it does. | | "I'll skip the custom functions for now" | Custom functions contain reusable business logic. Extract them early; they'll be referenced throughout. | | "This global field must be in PostgreSQL" | Global fields are UI/session state, not database state. Handle in application layer, not schema. | | "I can figure out relationships from field names alone" | FileMaker relationships include conditions. Check DDR relationship definitions for filtering rules. |
When extracting FileMaker business logic:
From user's existing codebase:
# program_catalog_parser.py:41-70
def get_ay_id(conn, catalog_year: str) -> Optional[str]:
"""
Get AY id from catalog_year string.
Converts full format "2025-2026" to short format "25-26"
and looks up corresponding AY record.
This logic was adapted from FileMaker calculation that
performed similar year format conversion in catalog imports.
"""
# Convert "2025-2026" to "25-26" (FileMaker custom function logic)
try:
ay_short = catalog_year[2:4] + '-' + catalog_year[7:9]
except IndexError:
logger.error(f"Invalid catalog_year format: {catalog_year}")
return None
with conn.cursor() as cur:
cur.execute("SELECT id FROM ay WHERE ay = %s", (ay_short,))
result = cur.fetchone()
if not result:
logger.warning(f"AY not found for catalog_year: {catalog_year}")
return None
return result[0]
This function adapted FileMaker's year conversion logic for use in PostgreSQL import scripts.
content-media
Download YouTube video transcripts when user provides a YouTube URL or asks to download/get/fetch a transcript from YouTube. Also use when user wants to transcribe or get captions/subtitles from a YouTube video.
development
Transform learning content (like YouTube transcripts, articles, tutorials) into actionable implementation plans using the Ship-Learn-Next framework. Use when user wants to turn advice, lessons, or educational content into concrete action steps, reps, or a learning quest.
tools
Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.
tools
Replace with description of the skill and when Claude should use it.