Detect unsafe contexts, queries in loops, hardcoded IDs, and more to optimize Salesforce Flows
Table of contents
Default Rules
📌Tip: To link directly to a specific rule, use the full GitHub anchor link format. Example:
https://flow-scanner.github.io/lightning-flow-scanner/#unsafe-running-context
Want to help improve this project? See our Contributing Guidelines
Problems
These rules detect anti-patterns and unsafe practices in your Flows that could break functionality, compromise security, or cause deployment failures.
DML Statement In A Loop
Executing DML operations (insert, update, delete) inside a loop is a high-risk anti-pattern that frequently causes governor limit exceptions. All database operations should be collected and executed once, outside the loop.
Rule ID: dml-in-loop
Class Name: DMLStatementInLoop
Severity: 🔴 Error
Hardcoded Salesforce Id
Avoid hard-coding record IDs, as they are unique to a specific org and will not work in other environments. Instead, store IDs in variables—such as merge-field URL parameters or a Get Records element—to make the Flow portable, maintainable, and flexible.
Rule ID: hardcoded-id
Class Name: HardcodedId
Severity: 🔴 Error
Hardcoded Salesforce Url
Avoid hard-coding URLs, as they may change between environments or over time. Instead, store URLs in variables or custom settings to make the Flow adaptable, maintainable, and environment-independent.
Rule ID: hardcoded-url
Class Name: HardcodedUrl
Severity: 🔴 Error
Hardcoded Secret 
Avoid hardcoding secrets, API keys, tokens, or credentials in Flows. These should be stored securely in Named Credentials, Custom Settings, Custom Metadata, or external secret management systems.
Rule ID: hardcoded-secret
Class Name: HardcodedSecret
Severity: 🔴 Error
Process Builder
Process Builder is retired. Continuing to use it increases maintenance overhead and risks future compatibility issues. Migrating automation to Flow reduces risk and improves maintainability.
Rule ID: process-builder-usage
Class Name: ProcessBuilder
Severity: 🔴 Error
SOQL Query In A Loop
Running SOQL queries inside a loop can rapidly exceed query limits and severely degrade performance. Queries should be executed once, with results reused throughout the loop.
Rule ID: soql-in-loop
Class Name: SOQLQueryInLoop
Severity: 🔴 Error
Unsafe Running Context
Flows configured to run in System Mode without Sharing grant access to all data, bypassing user permissions. Avoid this setting to prevent security risks and protect sensitive data.
Rule ID: unsafe-running-context
Class Name: UnsafeRunningContext
Severity: 🔴 Error
Duplicate DML Operation
When a Flow performs database operations across multiple screens, users navigating backward can cause the same actions to run multiple times. To prevent unintended changes, either restrict backward navigation or redesign the Flow so database operations execute in a single, forward-moving step.
Rule ID: duplicate-dml
Class Name: DuplicateDMLOperation
Severity: 🟡 Warning
Missing Fault Path
Elements that can fail should include a Fault Path to handle errors gracefully. Without it, failures show generic errors to users. Fault Paths improve reliability and user experience.
Rule ID: missing-fault-path
Class Name: MissingFaultPath
Severity: 🟡 Warning
Missing Null Handler
Get Records operations return null when no data is found. Without handling these null values, Flows can fail or produce unintended results. Adding a null check improves reliability and ensures the Flow behaves as expected.
Rule ID: missing-null-handler
Class Name: MissingNullHandler
Severity: 🟡 Warning
Recursive After Update
After-save Flows that update the same record can trigger recursion, causing unintended behavior or performance issues. Avoid updating the triggering record in after-save Flows; use before-save Flows instead to prevent recursion.
Rule ID: recursive-record-update
Class Name: RecursiveAfterUpdate
Severity: 🟡 Warning
Suggestions
These rules highlight areas where Flows can be improved. Following them increases reliability and long-term maintainability.
Action Call In A Loop
Repeatedly invoking Apex actions inside a loop can exhaust governor limits and lead to performance issues. Where possible, bulkify your logic by moving the action call outside the loop and passing a collection variable instead.
Rule ID: action-call-in-loop
Class Name: ActionCallsInLoop
Severity: 🟡 Warning
Get Record All Fields
Avoid using Get Records to retrieve all fields unless necessary. This improves performance, reduces processing time, and limits exposure of unnecessary data.
Rule ID: get-record-all-fields
Class Name: GetRecordAllFields
Severity: 🟡 Warning
Inactive Flow
Inactive Flows should be deleted or archived to reduce risk. Even when inactive, they can cause unintended record changes during testing or be activated as subflows. Keeping only active, relevant Flows improves safety and maintainability.
Rule ID: inactive-flow
Class Name: InactiveFlow
Severity: 🟡 Warning
Invalid API Version
Flows running on outdated API versions may behave inconsistently when newer platform features or components are used. From API version 50.0 onward, the API Version attribute explicitly controls Flow runtime behavior. Keeping Flows aligned with a supported API version helps prevent compatibility issues and ensures predictable execution.
Rule ID: invalid-api-version
Class Name: APIVersion
Severity: 🟡 Warning
Missing Filter Record Trigger 
Record-triggered Flows without filters on changed fields or entry conditions execute on every record change. Adding filters ensures the Flow runs only when needed, improving performance.
Rule ID: missing-record-trigger-filter
Class Name: MissingFilterRecordTrigger
Severity: 🟡 Warning
Same Record Field Updates
Before-save Flows can safely update the triggering record directly via $Record, applying changes efficiently without extra DML operations. Using before-save updates improves performance
Rule ID: same-record-field-updates
Class Name: SameRecordFieldUpdates
Severity: 🟡 Warning
Excessive Cyclomatic Complexity
High numbers of loops and decision elements increase a Flow’s cyclomatic complexity. To maintain simplicity and readability, consider using subflows or splitting a Flow into smaller, ordered Flows.
Rule ID: excessive-cyclomatic-complexity
Class Name: CyclomaticComplexity
Severity: 🔵 Note
Missing Trigger Order
Record-triggered Flows without a specified Trigger Order may execute in an unpredictable sequence. Setting a Trigger Order ensures your Flows run in the intended order.
Rule ID: unspecified-trigger-order
Class Name: TriggerOrder
Severity: 🔵 Note
Record ID as String 
Flows that use a String variable for a record ID instead of receiving the full record introduce unnecessary complexity and additional Get Records queries. Using the complete record simplifies the Flow and improves performance.
Rule ID: record-id-as-string
Class Name: RecordIdAsString
Severity: 🔵 Note
Transform Instead of Loop 
Loop elements that perform direct Assignments on each item can slow down Flows. Using Transform elements allows bulk operations on collections, improving performance and reducing complexity.
Rule ID: transform-instead-of-loop
Class Name: TransformInsteadOfLoop
Severity: 🔵 Note
Layout
Focused on naming, documentation, and organization, these rules ensure Flows remain clear, easy to understand, and maintainable as automations grow.
Flow Naming Convention
Using clear and consistent Flow names improves readability, discoverability, and maintainability. A good naming convention helps team members quickly understand a Flow’s purpose—for example, including a domain and brief description like Service_OrderFulfillment. Adopt a naming pattern that aligns with your organization’s standards.
Rule ID: invalid-naming-convention
Class Name: FlowName
Severity: 🔴 Error
Missing Flow Description
Flow descriptions are essential for documentation and maintainability. Include a description for each Flow, explaining its purpose and where it’s used.
Rule ID: missing-flow-description
Class Name: FlowDescription
Severity: 🔴 Error
Missing Metadata Description 
Elements and metadata without a description reduce clarity and maintainability. Adding descriptions improves readability and makes your automation easier to understand.
Rule ID: missing-metadata-description
Class Name: MissingMetadataDescription
Severity: 🟡 Warning
Unclear API Name
Elements with unclear or duplicated API names, like Copy_X_Of_Element, reduce Flow readability. Make sure to update the API name when copying elements to keep your Flow organized.
Rule ID: unclear-api-naming
Class Name: CopyAPIName
Severity: 🟡 Warning
Unreachable Element
Unconnected elements never execute and add unnecessary clutter. Remove or connect unused Flow elements to keep Flows clean and efficient.
Rule ID: unreachable-element
Class Name: UnconnectedElement
Severity: 🟡 Warning
Unused Variable
Unused variables are never referenced and add unnecessary clutter. Remove them to keep Flows efficient and easy to maintain.
Rule ID: unused-variable
Class Name: UnusedVariable
Severity: 🟡 Warning
Missing Auto Layout
Auto-Layout automatically arranges and aligns Flow elements, keeping the canvas organized and easier to maintain. Enabling it saves time and improves readability.
Rule ID: missing-auto-layout
Class Name: AutoLayout
Severity: 🔵 Note
Configuration
It is recommend to configure and define:
- The severity of violating any specific rule.
- Expressions used for rules, such as REGEX patterns and comparison operators.
- Any known exceptions that should be ignored during scanning.
{
"rules": {
// Your rule configurations
},
"exceptions": {
// Your defined exceptions
}
}
Most Lightning Flow Scanner distributions automatically resolve configurations from .flow-scanner.yml, .flow-scanner.json, or package.json → flowScanner.
Configure Rules
By default, all default rules are executed. You can customize individual rules and override the rules to be executed without having to specify every rule. Below is a breakdown of the available attributes of rule configuration:
{
"rules": {
"<RuleId>": {
"severity": "<Severity>", // Override severity level
"expression": "<Expression>", // Override rule expression
"message": "<Message>", // Set custom message
"messageUrl": "<URL>", // Set custom documentation URL
"enabled": false, // Disable this rule
}
}
}
Configure Severity
When the severity is not provided it will be warning by default. Other available values for severity are error and note. Configure the severity per rule as demonstrated below:
{
"rules": {
"record-id-as-string": {
"severity": "warning",
},
"unclear-api-naming": {
"severity": "error",
}
}
}
Override Expressions
Some rules are configurable and allow overriding their default expressions. You configure these overrides the same way as severity, as shown in the examples below.
{
"rules": {
"invalid-api-version": {
"expression": "===58" // comparison expression
},
"invalid-naming-convention": {
"expression": "[A-Za-z0-9]" // regular expression
}
}
}
Customize Messages
If not provided, message shows the standard rule summary and messageUrl links to the README; providing either overrides the default behavior.
{
"rules": {
"dml-in-loop": {
"message": "Avoid DML inside loops. Bulkify operations instead.",
"messageUrl": "https://internal.docs.company.com/salesforce/flow-dml-best-practices"
}
}
}
Disable Rules
To disable a rule, set "enabled": false as shown below:
{
"rules": {
"dml-in-loop": {
"enabled": false
}
}
}
Define Exceptions
Defining exceptions allows you to exclude specific scenarios from rule enforcement. Exceptions can be specified at the flow, rule, or result level to provide fine-grained control. Below is a breakdown of the available attributes of exception configuration:
{
"exceptions": {
"<FlowName>": {
"<RuleId>": [
"<ResultName>", // Suppress a result
"*", // Wildcard to suppress all results
...
]
},
...
}
}
Example
{
"exceptions": {
"MyFlow": {
"hardcoded-id": ["Old_Lookup_1"],
"missing-null-handler": ["*"]
}
}
}
Exclude Flows
Exclude by File Path (Node.js only)
Use glob patterns to exclude flows based on their file system location. This is useful for excluding entire directories or specific name patterns:
{
"ignore": [
"**/testing/**",
"**/*_Deprecated.flow-meta.xml"
]
}
Environment compatibility: requires Node.js(file system access) and is not available when using the Core Library in browser/web environments.
Exclude by Flow API Name (Browser-compatible)
Exclude specific flows by their unique API names, regardless of their location. This is particularly useful for:
- Excluding specific flows without knowing their exact file path
- Working with metadata API deployments where directory structures may vary
- More precise control than path-based patterns
{
"ignoreFlows": [
"My_Legacy_Flow",
"Temporary_Test_Flow",
"Deprecated_Process_Builder"
]
}
Environment compatibility: works in all environments including Node.js and browser/web distributions, as it operates on parsed flow data rather than file system paths.
Scan Modes
Beta Mode
New rules are introduced in Beta mode before being added to the default ruleset. To include current Beta rules, enable the optional betamode parameter in your configuration:
{ "betaMode": true }
Rule Mode
By default, Lightning Flow Scanner runs all default rules and merges any custom configurations you provide. If instead, you want to run only the rules you explicitly specify, use:
{ "ruleMode": "isolated" }
Installation
Distributions
| Distribution | Best for | Install |
|---|---|---|
| Salesforce CLI Plugin | Local development, scratch orgs, CI/CD | sf plugins install lightning-flow-scanner |
| VS Code Extension | Real-time scanning inside VS Code | code --install-extension ForceConfigControl.lightning-flow-scanner-vsx |
| Salesforce App (Managed Package) | Run scans directly inside a Salesforce org | sf package install --package 04tgK0000008CLlQAM |
| GitHub Action | Native PR checks | uses: Flow-Scanner/lightning-flow-scanner@main |
| Core Library (Node.js + Browser) | Custom tools, scripts, extensions, web apps | npm install -g @flow-scanner/lightning-flow-scanner-core |
Privacy: Zero user data collected. All processing is client-side. → See our Security Policy.
CICD Templates
Ready-to-use CI/CD templates and a Copado Plugin.
| Platform | Type | Link |
|---|---|---|
| Azure DevOps | Full Project Scan | azure-pipelines-flow-FullScan.yml |
| Azure DevOps | Change-Based Scan | azure-pipelines-flow-changedFiles.yml |
| Copado Plugin | Copado Plugin | Copado Marketplace |
Quick Start
Salesforce CLI Plugin
Use lightning-flow-scanner in the Salesforce CLI:
sf flow:scan # Scan flows in the current directory
sf flow:scan --sarif > report.sarif # Export scan results as SARIF
sf flow scan --csv > results.csv # Export scan results as CSV
sf flow doc > flow-docs.md # Generate flow documentation (Single markdown file)
sf flow doc --output flow-docs --separate # Generate one Markdown file per flow
sf flow:fix -d src/force-app # Fix flows in a specific directory
For full details, see the CLI Readme.
VS Code Extension
Use our side bar or the Command Palette and type flow scanner to see the list of all available commands.
Configure Scanner- Set up rules in.flow-scanner.ymlScan Flows- Analyze a directory or selected flow filesFix Flows- Automatically apply available fixesGenerate Flow Documentation- Generate flow documentationOpen Scanner Documentation- Open the rules reference guide
For full details, see the VSX Readme.
GitHub Action
Add a GitHub workflow file .github/workflows/scan-flows.yml to detect issues directly in pull requests:
- name: Lightning Flow Scan
id: flowscanner
uses: Flow-Scanner/lightning-flow-scanner@main
with:
sarif-only: true # Strict mode for PRs
- name: Upload SARIF to Code Scanning
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: $
For full details, see the Action Readme.
Core Module
Use lightning-flow-scanner-core as a Node.js/browser dependency:
// Basic
import { parse, scan } from "@flow-scanner/lightning-flow-scanner-core";
parse("flows/*.xml").then(scan);
// Get SARIF output (e.g. for GitHub Code Scanning)
import { parse, scan, exportSarif } from "@flow-scanner/lightning-flow-scanner-core";
parse("flows/**/*.flow-meta.xml").then(scan).then(exportSarif)
// .then(sarif => fs.writeFile("results.sarif", sarif))
// Generate Markdown documentation with Mermaid flow diagrams
import { parse, exportDiagram } from "@flow-scanner/lightning-flow-scanner-core";
parse("flows/**/*.flow-meta.xml").then(exportDiagram)
// .then(md => fs.writeFile("flow-docs.md", md))
// Browser Usage (Tooling API)
const { Flow, scan } = window.lightningflowscanner;
const metadataRes = await conn.tooling.query(`SELECT Id, FullName, Metadata FROM Flow`);
const results = scan(
metadataRes.records.map((r) => ({
uri: `/services/data/v60.0/tooling/sobjects/Flow/${r.Id}`,
flow: new Flow(r.FullName, r.Metadata),
})) //, optionsForScan
);
For more on Programmatic API, types, and advanced usage of @flow-scanner/lightning-flow-scanner-core, see the Core Library Reference.
Development
This project optionally uses Volta to guarantee the exact same Node.js and tool versions for every contributor.
MacOs/Linux:
curl https://get.volta.sh | bashWindows:
winget install Volta.VoltaVolta will automatically install and lock the tool versions defined in
package.json.
-
Clone the repository
git clone https://github.com/Flow-Scanner/lightning-flow-scanner.git -
Install dependencies:
pnpm install -
Compile:
pnpm run buildTo compile just the core package::
pnpm build:core -
Run tests:
pnpm testOr to test a new version of the core:
pnpm test:core -
Linking the core module locally(Optional):
To link the module, run:
pnpm link --global @flow-scanner/lightning-flow-scanner-coreYou can now do Ad-Hoc Testing with node:
node -i -e "import('@flow-scanner/lightning-flow-scanner-core').then(m => { Object.assign(global, m.default ? m.default : m); console.log('✅ Core loaded! Try: await parse(...), scan(...), etc.'); })"Or test in a dependent project with
npm link @flow-scanner/lightning-flow-scanner-core -
Deploy Demo Flows (Optional):
cd example-flows && sf project deploy startNavigate to the Demo Readme for full details
-
Create a standalone UMD Module(Optional):
pnpm distThis creates UMD at
dist/lightning-flow-scanner-core.umd.js.