| name | flow-parser |
| description | Use this skill for ANY task involving reading, parsing, or transforming Salesforce Flow XML (.flow-meta.xml files) into the internal FlowGraph model. Triggers include: parsing Flow elements, extracting connectors, detecting back-edges, handling fault paths, normalising Salesforce metadata into graph nodes and edges. Use this skill even if the user just says "parse the flow" or "read the XML" — it contains critical normalisation rules that prevent silent data loss.
|
Flow Parser Skill
Purpose
Parse Salesforce Flow XML (.flow-meta.xml) into the internal FlowGraph model
(defined in src/model/flow-graph.ts). Every parser implementation task must follow
this skill's rules.
Parser Configuration (fast-xml-parser)
Always initialise with these options — do not deviate:
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_',
parseAttributeValue: true,
isArray: (tagName) => [
'decisions',
'rules',
'connectors',
'assignments',
'loops',
'recordCreates',
'recordUpdates',
'recordDeletes',
'recordLookups',
'screens',
'actionCalls',
'subflows',
'collectionProcessors',
'assignmentItems',
'conditions',
'waitEvents',
'variables',
].includes(tagName),
});
The isArray config is critical. Salesforce produces single-element arrays as plain objects.
Without this, iterating over a single <decisions> element throws a "not iterable" error.
Element Extraction Rules
Start Node
const start = flow.Flow.start;
const startNode: FlowNode = {
id: 'start',
name: 'start',
label: 'Start',
type: 'start',
locationX: Number(start.locationX ?? 0),
locationY: Number(start.locationY ?? 0),
metadata: start,
};
Decisions
for (const decision of flow.Flow.decisions ?? []) {
const node: FlowNode = { id: decision.name, name: decision.name, label: decision.label, type: 'decision', ... };
for (const rule of decision.rules ?? []) {
edges.push({
id: `${decision.name}__${rule.name}`,
sourceId: decision.name,
targetId: rule.connector.targetReference,
label: rule.label,
isFault: false,
isBackEdge: false,
});
}
if (decision.defaultConnector) {
edges.push({
id: `${decision.name}__default`,
sourceId: decision.name,
targetId: decision.defaultConnector.targetReference,
label: decision.defaultConnectorLabel ?? 'Default',
isFault: false,
isBackEdge: false,
});
}
}
Loops
Loops have two outgoing connectors:
nextValueConnector → processes next iteration element
noMoreValuesConnector → exits the loop
edges.push({
id: `${loop.name}__next`,
sourceId: loop.name,
targetId: loop.nextValueConnector.targetReference,
label: 'For Each',
isFault: false,
isBackEdge: false,
});
edges.push({
id: `${loop.name}__done`,
sourceId: loop.name,
targetId: loop.noMoreValuesConnector.targetReference,
label: 'After Last',
isFault: false,
isBackEdge: false,
});
Fault Connectors
All DML elements (recordCreates, recordUpdates, recordDeletes, recordLookups, actionCalls)
may have a <faultConnector>. Always check for it:
if (element.faultConnector) {
edges.push({
id: `${element.name}__fault`,
sourceId: element.name,
targetId: element.faultConnector.targetReference,
label: 'Fault',
isFault: true,
isBackEdge: false,
});
}
Back-Edge Detection
Run this AFTER all nodes and edges are built. Uses iterative DFS to avoid stack overflow on large flows.
function detectBackEdges(graph: FlowGraph): void {
const visited = new Set<string>();
const pathStack = new Set<string>();
function dfs(nodeId: string): void {
visited.add(nodeId);
pathStack.add(nodeId);
const outgoing = graph.edges.filter(e => e.sourceId === nodeId);
for (const edge of outgoing) {
if (!visited.has(edge.targetId)) {
dfs(edge.targetId);
} else if (pathStack.has(edge.targetId)) {
edge.isBackEdge = true;
}
}
pathStack.delete(nodeId);
}
dfs('start');
}
Coordinate Extraction
function extractCoords(element: unknown): { locationX: number; locationY: number } {
const el = element as Record<string, unknown>;
const x = Number(el['locationX'] ?? 0);
const y = Number(el['locationY'] ?? 0);
return {
locationX: isNaN(x) ? 0 : x,
locationY: isNaN(y) ? 0 : y,
};
}
Never throw on missing coordinates. Return { locationX: 0, locationY: 0 } and let the
layout engine handle placement.
Test Fixtures Required
Every parser task must have a corresponding .flow-meta.xml fixture in tests/fixtures/.
Minimum fixture set:
simple-linear.flow-meta.xml — start + 2 assignments + screen, no branches
decision-with-rules.flow-meta.xml — 3 rules + default connector
loop-with-subflow.flow-meta.xml — loop containing a subflow reference
fault-path.flow-meta.xml — record update with fault connector
large-flow.flow-meta.xml — 25+ nodes (generated or real)
Definition of Done (Parser)
Variable Parsing
Add 'variables' to the isArray config list so single-variable flows don't break.
Each <variables> element has these fields — extract all of them:
const variablesRaw = (flow['variables'] as Record<string, unknown>[] | undefined) ?? [];
const variables: FlowVariable[] = variablesRaw.map((v) => ({
name: String(v['name'] ?? ''),
dataType: String(v['dataType'] ?? 'String'),
isCollection: v['isCollection'] === true || String(v['isCollection']) === 'true',
isInput: v['isInput'] === true || String(v['isInput']) === 'true',
isOutput: v['isOutput'] === true || String(v['isOutput']) === 'true',
}));
Boolean coercion note: fast-xml-parser with parseAttributeValue: true may parse 'true'/'false' strings as booleans in attributes but leave them as strings in element content. Guard both cases with the === true || === 'true' pattern.
Return variables on the FlowGraph object. The field is optional (variables?: FlowVariable[]) to avoid breaking inline test fixtures that construct FlowGraph without variables.