From cirra-ai-skills
Creates and validates Salesforce flows with 110-point scoring and Winter '26 best practices using Cirra AI MCP Server. Use when building record-triggered flows, screen flows, autolaunched flows, scheduled flows, or reviewing existing flow performance. Usage: /sf-flow [create|update|validate] {FlowName} ...
npx claudepluginhub cirra-ai/skillsThis skill uses the workspace's default tool permissions.
Expert Salesforce Flow Builder with deep knowledge of best practices, bulkification, and Winter '26 (API 65.0) metadata. Create production-ready, performant, secure, and maintainable flows using Cirra AI MCP Server for deployment.
CREDITS.mdLICENSEREADME.mdagents/openai.yamlassets/apex-action-template.xmlassets/architecture-review-template.mdassets/autolaunched-flow-template.xmlassets/bypass-check-decision.xmlassets/elements/get-records-pattern.xmlassets/elements/loop-pattern.xmlassets/elements/record-delete-pattern.xmlassets/elements/transform-pattern.xmlassets/error-logging-example.mdassets/flow-documentation-template.mdassets/icon-large.pngassets/icon-small.pngassets/json-deployment-reference.mdassets/multi-step-dml-rollback-example.mdassets/orchestration-conditional.mdassets/orchestration-parent-child.mdGuides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Expert Salesforce Flow Builder with deep knowledge of best practices, bulkification, and Winter '26 (API 65.0) metadata. Create production-ready, performant, secure, and maintainable flows using Cirra AI MCP Server for deployment.
Parse $ARGUMENTS to determine the action:
| First argument or intent | Workflow |
|---|---|
create, new flow request | Create Flow |
update, modify existing flow | Update Flow |
validate, review, score | Validate Flow |
| (no argument or unclear) | Ask the user (see below) |
When the operation is missing or unclear, you MUST use AskUserQuestion before proceeding:
AskUserQuestion(question="What would you like to do?\n\n1. **Create** — generate a new Flow\n2. **Update** — fetch, modify, validate, and redeploy\n3. **Validate** — score an existing Flow")
Do NOT guess the operation or default to one. Wait for the user's answer.
Create a new Flow following Winter '26 best practices.
Use AskUserQuestion to collect:
Before generating, confirm the flow doesn't already exist:
metadata_list(
type="Flow",
sf_user="<sf_user>"
)
If it exists, suggest running with update <FlowApiName> instead.
Create the flow XML following the sf-flow skill guidelines (see Workflow Design section below):
runInMode="SystemModeWithoutSharing" only where justifiedWrite the generated XML to a temp file and validate:
python3 "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/validate_flow_cli.py" "/tmp/<FlowApiName>.flow-meta.xml"
Fix any CRITICAL or HIGH issues before proceeding. The pre-deployment hook will also validate automatically when metadata_create is called.
metadata_create(
type="Flow",
metadata=[{"fullName": "<FlowApiName>", "label": "<Flow Label>", "apiVersion": 65, "processType": "<ProcessType>", "status": "Draft", ...}]
)
Show the final validation score and deployment status.
Fetch, modify, validate, and redeploy an existing Salesforce Flow.
The argument should be a flow API name: update Auto_Lead_Assignment do X
If no flow name is given, ask the user which flow to update and what changes are needed.
metadata_read(
type="Flow",
fullNames=["<FlowApiName>"],
sf_user="<sf_user>"
)
If the flow is not found, suggest running with create instead.
Review the existing flow XML before making any changes. Understand:
Modify the flow following sf-flow skill guidelines. Preserve:
Write the updated XML to a temp file and validate:
python3 "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/validate_flow_cli.py" "/tmp/<FlowApiName>.flow-meta.xml"
Fix any CRITICAL or HIGH issues before proceeding. The pre-deployment hook will also validate automatically when metadata_update is called.
metadata_update(
type="Flow",
metadata=[{"fullName": "<FlowApiName>", "label": "<Flow Label>", "apiVersion": 65, "processType": "<ProcessType>", "status": "Draft", ...}]
)
Summarise the changes made and show the final validation score.
Validate one or more Flows using the 110-point static analysis pipeline and return a scored report.
Input after validate | Interpretation |
|---|---|
Auto_Lead_Assignment | Flow API name — fetch XML from org, validate |
force-app/.../Auto_Lead_Assignment.flow-meta.xml (ends .flow-meta.xml or .xml) | Local file — validate directly |
Auto_Lead_Assignment,Screen_Case_Intake | Comma-separated list — bulk fetch, validate each |
All | All Flow records in the org |
| (no argument) | Ask the user what to validate |
The validation script is at ${CLAUDE_PLUGIN_ROOT}/hooks/scripts/validate_flow_cli.py. Locate it with:
# $CLAUDE_PLUGIN_ROOT is set by Claude Code. Other hosts: see references/execution-modes.md.
# If not set, find the script:
find ~/.claude/plugins -name "validate_flow_cli.py" 2>/dev/null | grep sf-flow | head -1
python3 "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/validate_flow_cli.py" "<file_path>"
metadata_read(
type="Flow",
fullNames=["<FlowApiName>"],
sf_user="<sf_user>"
)
Write /tmp/validate_<FlowApiName>.flow-meta.xml ← the flow XML
python3 "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/validate_flow_cli.py" "/tmp/validate_<FlowApiName>.flow-meta.xml"
Fetch all flow XML bodies in a single call:
metadata_read(
type="Flow",
fullNames=["Flow1", "Flow2", "Flow3"],
sf_user="<sf_user>"
)
Fallback: If the bulk read fails (timeout or size error), fall back to individual metadata_read calls per flow.
Validate each flow body (write → validate → delete). After all flows are validated, show a summary table sorted by score ascending (worst first):
| Flow | Score | % | Status |
|---|---|---|---|
| Before_Opportunity_Validate | 72/110 | 65% | Below threshold |
| Auto_Lead_Assignment | 98/110 | 89% | Pass |
metadata_list(type="Flow", sf_user="<sf_user>")
metadata_read(
type="Flow",
fullNames=["Flow1", ..., "Flow20"],
sf_user="<sf_user>"
)
Backoff strategy: If a batch of 20 fails (timeout or response size error), retry with 10, then 5, then fall back to individual reads for that batch.
Flow Creation & Deployment Workflow:
1. Call cirra_ai_init (REQUIRED - one per session)
2. Generate Flow metadata (JSON object — NOT XML)
3. Deploy via metadata_create tool (Cirra AI MCP Server)
4. Retrieve existing flows via metadata_read or metadata_list (Cirra AI MCP Server)
5. Query Flow metadata via tooling_api_query for FlowDefinition
6. Describe objects/fields via sobject_describe before flow creation
Scoring: 110 points across 6 categories. Minimum 88 (80%) for deployment. Trivial flows (single-step automations, test/throwaway flows) are exempt from the minimum threshold — score them for informational purposes but do not block deployment. Guardrail anti-pattern checks (DML in loops, missing fault paths) still apply regardless of complexity.
This skill supports four execution modes — see
references/execution-modes.md for detection logic and full details,
and references/mcp-pagination.md for handling large MCP responses.
All Flow operations go through MCP tools regardless of mode. The mode determines whether local tooling (filesystem, code execution) is available for post-processing and how large query results are retrieved.
BEFORE using any Cirra AI tools:
cirra_ai_init()
Call with no parameters — uses the default org. If a default is configured, confirm with the user before proceeding. If no default is configured, ask for the Salesforce user/alias.
This initializes your Salesforce org connection. It must be called once per session before using any of these Cirra AI tools:
metadata_create (deploy flows)metadata_read (retrieve flows)metadata_list (list existing flows)tooling_api_query (query FlowDefinition)sobject_describe (verify objects/fields)soql_query (query org data)sf-metadata → sf-flow → sf-data (you are here: sf-flow with Cirra AI)
⚠️ Flow references custom object/fields? Create with sf-metadata FIRST. Deploy objects BEFORE flows.
1. sf-metadata → Create objects/fields (local)
2. sf-flow ◀── YOU ARE HERE (create flow, deploy via Cirra AI)
3. sf-data → Create test data (remote - objects must exist!)
See references/orchestration.md for extended orchestration patterns including Agentforce.
| Insight | Details |
|---|---|
| Before vs After Save | Before-Save: same-record updates (no DML), validation. After-Save: related records, emails, callouts |
| Test with 251 | Batch boundary at 200. Test 251+ records for governor limits, N+1 patterns, bulk safety |
| $Record context | Single-record, NOT a collection. Platform handles batching. Never loop over $Record |
| $Record traversal | $Record supports relationship traversal: {!$Record.Contact__r.FirstName}, {!$Record.Account__r.Name}. Do NOT use Get Records for data already available through $Record lookups — this wastes a SOQL query |
| Transform vs Loop | Transform: data mapping/shaping (30-50% faster). Loop: per-record decisions, counters, varying logic. See references/transform-vs-loop-guide.md |
For simple, self-contained flows (single record update, basic field mapping, straightforward screen flow), bypass the detailed requirements/design elaboration and full scoring while still performing initialization and mandatory guardrails, then generate + deploy:
cirra_ai_init() (always required)sobject_describe to verify the target object/fields existmetadata_createUse the fast path when: the request is explicit, the flow is a single straightforward automation, and there are no ambiguous requirements.
Use the full 5-phase workflow when: the flow involves multiple decision branches, screen flows with complex logic, subflow orchestration, or underspecified requirements.
Before building, evaluate alternatives: See references/flow-best-practices.md Section 1 "When NOT to Use Flow" - sometimes a Formula Field, Validation Rule, or Roll-Up Summary Field is the better choice.
If the request is underspecified, ask concise follow-up questions to gather:
Pre-Development Planning: For complex flows, document requirements and sketch logic before building. See references/flow-best-practices.md Section 2 "Pre-Development Planning" for templates and recommended tools.
Then:
cirra_ai_init() with no parameters. If a default org is configured, confirm with the user. If no default, ask for the Salesforce user/alias before proceeding.sobject_describe to verify object/field existence before referencingmetadata_list to check existing flows: metadata_list(type="Flow")references/subflow-library.mdreferences/governance-checklist.mdSelect template:
| Flow Type | Template File |
|---|---|
| Screen | screen-flow-template.xml |
| Record-Triggered | record-triggered-*.xml |
| Platform Event | platform-event-flow-template.xml |
| Autolaunched | autolaunched-flow-template.xml |
| Scheduled | scheduled-flow-template.xml |
| Wait Elements | wait-template.xml |
Element Pattern Templates (assets/elements/):
| Element | Template | Purpose |
|---|---|---|
| Loop | loop-pattern.xml | Complete loop with nextValueConnector/noMoreValuesConnector |
| Get Records | get-records-pattern.xml | All recordLookups options (filters, sort, limit) |
| Delete Records | record-delete-pattern.xml | Filter-based and reference-based delete patterns |
JSON Deployment Reference (assets/json-deployment-reference.md):
Covers XML-to-JSON translation, property placement rules, start patterns for all flow types, entry conditions (filterFormula vs filters), value reference patterns, and element JSON examples. For metadata_create deployments, this reference alone is usually sufficient — the XML templates are optional structural references for complex or unfamiliar flow types.
Template Path Resolution (try in order):
assets/[template]assets/elements/[template]When to read XML templates: Only when dealing with complex or unfamiliar element patterns (e.g., wait elements, advanced screen flows). For standard record-triggered, autolaunched, and scheduled flows, the JSON deployment reference has all the patterns needed.
Example: Read: assets/record-triggered-after-save.xml
Naming Convention (Recommended Prefixes):
| Flow Type | Prefix | Example |
|---|---|---|
| Record-Triggered (After) | Auto_ | Auto_Lead_Assignment, Auto_Account_Update |
| Record-Triggered (Before) | Before_ | Before_Lead_Validate, Before_Contact_Default |
| Screen Flow | Screen_ | Screen_New_Customer, Screen_Case_Intake |
| Scheduled | Sched_ | Sched_Daily_Cleanup, Sched_Weekly_Report |
| Platform Event | Event_ | Event_Order_Completed |
| Autolaunched | Sub_ or Util_ | Sub_Send_Email, Util_Validate_Address |
Format: [Prefix]_Object_Action using PascalCase (e.g., Auto_Lead_Priority_Assignment)
Screen Flow Button Config (CRITICAL):
| Screen | allowBack | allowFinish | Result |
|---|---|---|---|
| First | false | true | "Next" only |
| Middle | true | true | "Previous" + "Next" |
| Last | true | true | "Finish" |
Rule: allowFinish="true" required on all screens. Connector present → "Next", absent → "Finish".
Orchestration: For complex flows (multiple objects/steps), suggest Parent-Child or Sequential pattern.
references/xml-gotchas.md and references/orchestration-guide.mdTwo deployment formats — know which to use:
Path Format When metadata_create/metadata_updateJSON object Deploying via Cirra AI MCP Server Writing .flow-meta.xmltoforce-app/XML Source-controlled project files CRITICAL: Do NOT pass XML strings to
metadata_create. It requires a structured JSON object — use the format reference and examples below. The XML templates inassets/are the correct reference when writing local.flow-meta.xmlfiles.
Generate flow metadata: Construct the complete Flow metadata as a JSON object with:
label, processType, status, etc.)CRITICAL Requirements:
bulkSupport property (removed API 60.0+)locationX/locationY = 0Read
assets/json-deployment-reference.mdfor the complete reference — it covers XML-to-JSON translation, start patterns for all flow types, entry conditions, value references, and element JSON examples.
Essential rules (always apply):
metadata_create requires a JSON object, NOT XML. The XML templates
in assets/ show structure; translate using the reference above.triggerType, recordTriggerType, object, schedule,
filters/filterFormula/filterLogic belong ONLY inside start, never at top level.{"stringValue": "text"}, {"booleanValue": true},
{"numberValue": 100}, {"elementReference": "var_Name"}.stringValue supports {!$Record.Name} syntax — no need for
formula variables for simple string interpolation.filterFormula for compound/negated conditions
(AND(), OR(), NOT()). Use filters array for simple field comparisons.Pre-Deployment: Check Prerequisites (REQUIRED for flows referencing custom fields/objects):
Before deploying a flow, verify that all referenced custom fields and objects exist
in the target org. Flows referencing missing fields will deploy but become
InvalidDraft and cannot be activated.
# Check if custom field exists before deploying flow that references it
sobject_describe(sObject="Lead")
# Verify TEST_Priority__c (or any custom field) appears in the field list
# If missing: create the field FIRST via sobject_field_create, then deploy the flow
Deploy via Cirra AI:
# Initialize connection (ONCE per session)
cirra_ai_init(sf_user="your-username")
# Create/deploy Flow — pass a JSON object, NOT XML
metadata_create(
type="Flow",
metadata=[{
"fullName": "Auto_Lead_Assignment",
"label": "Auto Lead Assignment",
"apiVersion": 65,
"description": "Assigns new leads to the appropriate queue based on region",
"environments": ["Default"],
"processMetadataValues": [
{"name": "BuilderType", "value": {"stringValue": "LightningFlowBuilder"}},
{"name": "CanvasMode", "value": {"stringValue": "AUTO_LAYOUT_CANVAS"}}
],
"processType": "AutoLaunchedFlow",
"start": {
"locationX": 0, "locationY": 0,
"object": "Lead",
"recordTriggerType": "Create",
"triggerType": "RecordAfterSave",
"connector": {"targetReference": "Check_Region"}
},
"decisions": [...],
"recordUpdates": [...],
"status": "Draft"
}],
sf_user="your-username"
)
Post-Deployment: Verify Flow Status (REQUIRED after every metadata_create for Flow):
After deploying a flow, immediately query its status via the Tooling API to
detect InvalidDraft. This catches issues the Metadata API accepts silently.
# Check flow status after deployment
tooling_api_query(
sObject="Flow",
fields=["Id", "Definition.DeveloperName", "VersionNumber", "Status"],
whereClause="Definition.DeveloperName = 'Auto_Lead_Assignment'"
)
# Expected: Status = "Draft"
# If Status = "InvalidDraft":
# 1. Check for missing triggerType (scheduled flows need triggerType=Scheduled)
# 2. Check for missing custom field references (sobject_describe to verify)
# 3. Fix the issue and redeploy via metadata_update
Common InvalidDraft Causes and Fixes:
| Cause | Symptom | Fix |
|---|---|---|
Missing triggerType in start | Scheduled flow with schedule but no triggerType: Scheduled | Add triggerType: "Scheduled" to start element |
| Missing custom field | Flow references Custom_Field__c that doesn't exist | Create field via sobject_field_create first, then redeploy |
Deprecated bulkSupport | API 60.0+ flow includes bulkSupport | Remove the bulkSupport property |
Missing recordTriggerType | Record-triggered flow without recordTriggerType | Add recordTriggerType: "Create" (or Update/CreateAndUpdate) |
For Review — validate an existing flow from the org or a local file before modifying:
python scripts/validate_flow_cli.py <FlowApiName> — fetch and validate a single flow from the orgpython scripts/validate_flow_cli.py All — full org audit sorted by scoreValidation (STRICT MODE):
New v2.0.0 Validations:
storeOutputAutomatically detection (data leak prevention)Validation Report Format (6-Category Scoring 0-110):
Score: 92/110 ⭐⭐⭐⭐ Very Good
├─ Design & Naming: 18/20 (90%)
├─ Logic & Structure: 20/20 (100%)
├─ Architecture: 12/15 (80%)
├─ Performance & Bulk Safety: 20/20 (100%)
├─ Error Handling: 15/20 (75%)
└─ Security: 15/15 (100%)
Strict Mode: If ANY errors/warnings → Block with options: (1) Apply auto-fixes, (2) Show manual fixes, (3) Generate corrected version. DO NOT PROCEED until 100% clean.
BEFORE generating ANY Flow metadata, VERIFY no anti-patterns are introduced.
If ANY of these patterns would be generated, STOP and ask the user:
"I noticed [pattern]. This will cause [problem]. Should I: A) Refactor to use [correct pattern] B) Proceed anyway (not recommended)"
| Anti-Pattern | Impact | Correct Pattern |
|---|---|---|
| After-Save updating same object without entry conditions | Infinite loop (critical) | MUST add entry conditions: "Only when [field] is changed" |
| Get Records inside Loop | Governor limit failure (100 SOQL) | Query BEFORE loop, use collection variable |
| Create/Update/Delete Records inside Loop | Governor limit failure (150 DML) | Collect in loop → single DML after loop |
| Apex Action inside Loop | Callout limits | Pass collection to single Apex invocation |
| DML without Fault Path | Silent failures | Add Fault connector → error handling element |
| Get Records without null check | NullPointerException | Add Decision: "Records Found?" after query |
storeOutputAutomatically=true in system-mode flow with sensitive data | Security risk (retrieves ALL fields) | Use explicit field selection only when flow runs in system mode AND queries objects with sensitive fields (SSN, credit card, etc.) |
| Query same object as trigger in Record-Triggered | Wasted SOQL | Use {!$Record.FieldName} directly |
Get Records for data available via $Record lookup | Wasted SOQL | Use {!$Record.Lookup__r.Field} — traversal works up to 5 levels |
| Hardcoded Salesforce ID | Deployment failure across orgs | Use input variable or Custom Label |
| Get Records without filters | Too many records returned | Always include WHERE conditions |
DO NOT generate anti-patterns even if explicitly requested. Ask user to confirm the exception with documented justification.
Cirra AI Deployment Pattern:
cirra_ai_init()
Automatic validation: A skill-scoped PreToolUse hook runs
pre-mcp-validate.pybefore everymetadata_create,metadata_update, andtooling_api_dmlcall while this skill is active. It blocks deployment for CRITICAL/HIGH issues (DML in loops, missing fault paths) and warns when score is below 80% (88/110).
# Pass a structured JSON object — see cirra_ai_init instructions for format examples
metadata_create(
type="Flow",
metadata=[{
"fullName": "Auto_Lead_Assignment",
"label": "Auto Lead Assignment",
"apiVersion": 65,
"processType": "AutoLaunchedFlow",
"status": "Draft",
# ... full flow structure as JSON properties
}],
sf_user="your-salesforce-username"
)
metadata_read(
type="Flow",
fullNames=["Auto_Lead_Assignment"],
sf_user="your-salesforce-username"
)
metadata_list(
type="Flow",
sf_user="your-salesforce-username"
)
tooling_api_query(
sObject="FlowDefinition",
fields=["Id", "ApiName", "Description"],
whereClause="Status = 'Active'",
sf_user="your-salesforce-username"
)
sobject_describe(
sObject="Account",
sf_user="your-salesforce-username"
)
For Agentforce Flows: Variable names must match Agent Script input/output names exactly.
For complex flows: references/governance-checklist.md
Type-specific testing: See references/testing-guide.md | references/testing-checklist.md | references/wait-patterns.md (Wait element guidance)
Quick reference:
Best Practices: See references/flow-best-practices.md for:
Security: Test with multiple profiles. System mode requires security review.
Completion Summary:
✓ Flow Creation & Deployment Complete: [FlowName]
Type: [type] | API: 65.0 | Status: [Draft/Active]
Deployed via: Cirra AI MCP Server (metadata_create)
Validation: PASSED (Score: XX/110)
Org: [target-org-username]
Navigate: Setup → Process Automation → Flows → "[FlowName]"
Next Steps: Test (unit, bulk, security), Review docs, Activate if Draft, Monitor logs
Resources: `assets/`, `references/subflow-library.md`, `references/orchestration-guide.md`, `references/governance-checklist.md`
NEVER loop over triggered records. $Record = single record; platform handles batching.
| Pattern | OK? | Notes |
|---|---|---|
$Record.FieldName | ✅ | Direct field access |
$Record.Lookup__r.FieldName | ✅ | Relationship traversal — NO Get Records needed |
$Record.Account__r.Owner.Name | ✅ | Multi-level traversal (up to 5 levels) |
Get Records for $Record lookup | ❌ | Wastes SOQL — use $Record.Relationship__r.Field instead |
Loop over $Record__c | ❌ | Process Builder pattern, not Flow |
Loop over $Record | ❌ | $Record is single, not collection |
$Record relationship traversal: In record-triggered flows, $Record provides access to related records through lookup/master-detail fields WITHOUT a Get Records element. Use {!$Record.Contact__r.FirstName} instead of querying Contact separately. Only use Get Records when you need related records that are NOT accessible through $Record lookups (e.g., child records, or records with no relationship to the trigger object).
Loops for RELATED records only: Get Records → Loop collection → Assignment → DML after loop
recordLookups cannot query Parent.Field (e.g., Manager.Name). Solution: Two Get Records - child first, then parent by Id.
| Element | Recommendation | Why |
|---|---|---|
getFirstRecordOnly | Set to true for single-record queries | Avoids collection overhead |
storeOutputAutomatically | Set to true (default) | Simpler, modern approach — auto-stores all fields. Only set to false with explicit field selection when handling sensitive data in system-mode flows |
assignNullValuesIfNoRecordsFound | Set to false | Preserves previous variable value |
faultConnector | Always include | Handle query failures gracefully |
filterLogic | Use and for multiple filters | Clear filter behavior |
All properties of the same type MUST be grouped together. Do NOT scatter them across the object.
Complete alphabetical order:
apiVersion → assignments → constants → decisions → description → environments →
formulas → interviewLabel → label → loops → processMetadataValues → processType →
recordCreates → recordDeletes → recordLookups → recordUpdates → runInMode →
screens → start → status → subflows → textTemplates → variables → waits
Common Mistake: Adding an assignment near related logic (e.g., after a loop) when other assignments exist earlier.
var_ Regular variables (e.g., var_AccountName)col_ Collections (e.g., col_ContactIds)rec_ Record variables (e.g., rec_Account)inp_ Input variables (e.g., inp_RecordId)out_ Output variables (e.g., out_IsSuccess)Check_Account_Type)Action_[Verb]_[Object] (e.g., Action_Save_Contact)references/flow-best-practices.md for comprehensive guidanceDML in Loop: Collect records in collection variable → Single DML after loop Missing Fault Path: Add fault connector from DML → error handling → log/display Self-Referencing Fault: Error "element cannot be connected to itself" → Route fault connector to DIFFERENT element Element Duplicated: Error "Element X is duplicated" → Group ALL elements of same type together Field Not Found: Verify field exists, deploy field first if missing Insufficient Permissions: Check profile permissions, consider System mode
| Error Pattern | Fix |
|---|---|
$Record__Prior in Create-only | Only valid for Update/CreateAndUpdate triggers |
| "Parent.Field doesn't exist" | Use TWO Get Records (child then parent) |
$Record__c loop fails | Use $Record directly (single context, not collection) |
| Error Message | Solution |
|---|---|
Duplicate developer name: X | Screen field already created this reference — don't add a separate variable |
Can't use object field with sObjectInputReference | Remove object property when using inputReference |
isCollection invalid in FlowConstant | Use Decision + Variable counter instead of a constant collection |
Invalid element reference X not found | Check all element names are unique and connectors point to existing elements |
| Flow won't open in Flow Builder | Add all empty element type arrays to flow metadata |
Silent failure on metadata_update | Read current state first with metadata_read; build iteratively |
| Required field missing | Add processMetadataValues: [] to every element |
Metadata Gotchas: See references/xml-gotchas.md
These lessons apply when creating or updating flows via metadata_create / metadata_update (JSON format). They are based on real-world failures and must be followed to avoid deployment errors.
Screen fields automatically create element references with their field name. Do NOT create a separate variable for screen fields — this causes a Duplicate developer name error.
// WRONG: Don't create variable with same name as screen field
{ "screens": [{ "fields": [{ "name": "User_Input" }] }],
"variables": [{ "name": "User_Input" }] // DUPLICATE ERROR }
// CORRECT: Reference the screen field directly
{ "formulas": [{ "expression": "{!User_Input} & \" suffix\"" }] }
object and inputReferenceWhen creating records from a collection using inputReference, do NOT include the object field. Define objectType on the variable instead.
// WRONG:
{ "name": "Create_All", "object": "Account", "inputReference": "Var_Col" }
// CORRECT: objectType goes on the variable, not the create element
{ "variables": [{ "name": "Var_Col", "dataType": "SObject",
"objectType": "Account", "isCollection": true }],
"recordCreates": [{ "name": "Create_All", "inputReference": "Var_Col" }] }
Flow constants cannot be collections or SObjects. Use a Decision + Counter Variable instead.
ALWAYS call metadata_read immediately before metadata_update for complex changes. Salesforce replaces the entire flow version on update — working with stale metadata will overwrite recent changes.
metadata_read to get current statemetadata_update with complete stateEvery element name in a Flow must be unique across ALL element types. Use prefixes to enforce this:
| Element Type | Naming Convention | Example |
|---|---|---|
| Variables | Var_* | Var_Account_Id |
| Formulas | Formula_* | Formula_Full_Name |
| Screens | Screen_* | Screen_Welcome |
| Decisions | Decision_* | Decision_Route |
| Assignments | Assign_* | Assign_Defaults |
| Choices | Choice_* | Choice_Option_A |
| Screen Fields | Descriptive | Account_Name |
metadata_create with minimal structure. Test: Does it open in Flow Builder?Red flags: Creating 100+ line JSON on first attempt. Adding 5+ new element types at once. Not testing in Flow Builder between changes.
Never create records one-by-one in a loop. Build a collection, then execute a single DML operation:
Var_Current_Record (single SObject variable)Add to append to the collection variablerecordCreates with inputReference pointing to the collectionSynchronous DML limit: 150 statements. Creating 10 records individually = 10 DML. Creating 10 records via collection = 1 DML.
processMetadataValues — Always Include Empty ArrayEvery Flow element MUST have a processMetadataValues property, even if it's an empty array. Without it, Salesforce may fail silently or discard the element.
{ "name": "Element_Name", "processMetadataValues": [] }
Include ALL element type arrays in your flow metadata, even if empty. Missing arrays can cause silent failures, elements being deleted, or flows that won't save.
{
"assignments": [],
"choices": [],
"decisions": [],
"formulas": [],
"loops": [],
"recordCreates": [],
"recordDeletes": [],
"recordLookups": [],
"recordUpdates": [],
"screens": [],
"variables": [],
"textTemplates": []
}
Every element (except the final one) must have a connector to the next element. Mental checklist for each element:
connector?defaultConnector?targetReference point to an existing element?allowFinish: true)?metadata_create)Always start with this complete template — include ALL empty arrays:
{
"fullName": "PLACEHOLDER",
"label": "PLACEHOLDER",
"apiVersion": 65,
"processType": "Flow",
"status": "Draft",
"interviewLabel": "PLACEHOLDER {!$Flow.CurrentDateTime}",
"environments": ["Default"],
"processMetadataValues": [
{ "name": "BuilderType", "value": { "stringValue": "LightningFlowBuilder" } },
{ "name": "CanvasMode", "value": { "stringValue": "AUTO_LAYOUT_CANVAS" } }
],
"start": {
"locationX": 0,
"locationY": 0,
"connector": { "targetReference": "FIRST_ELEMENT" },
"filters": [],
"processMetadataValues": []
},
"assignments": [],
"choices": [],
"decisions": [],
"formulas": [],
"loops": [],
"recordCreates": [],
"recordDeletes": [],
"recordLookups": [],
"recordUpdates": [],
"screens": [],
"variables": [],
"textTemplates": []
}
| Element Type | Purpose | Key Notes |
|---|---|---|
| Start | Entry point | Contains connector to first element; record-triggered adds filters/object |
| Variables | Store values | Counter vars: dataType Number, scale 0. Collections: isCollection true |
| Screens | User interface | Fields auto-create element references — do NOT create duplicate variables |
| Decisions | Branching logic | Must always include defaultConnector |
| Record Lookups | Query Salesforce data | Use storeOutputAutomatically: false for security |
| Record Creates | Insert new records | Use inputReference for collections — never combine with object field |
| Assignments | Set variable values | Operators: Assign, Add, AssignCount |
| Loops | Iterate collections | Auto-creates currentItem_{LoopName} variable |
| Formulas | Computed values | Use {!VarName} syntax to reference other elements |
| Scenario | Solution |
|---|---|
| >200 records | Warn limits, suggest scheduled flow |
| >5 branches | Use subflows |
| Cross-object | Check circular deps, test recursion |
| Production | Deploy Draft, activate explicitly |
| Unknown org | Use standard objects (Account, Contact, etc.) |
Debug: Flow not visible → deploy report + permissions | Tests fail → Debug Logs + bulk test | Sandbox→Prod fails → FLS + dependencies
start block) instead of a Decision with an empty actionCanvasMode: AUTO_LAYOUT_CANVAS)tooling_api_query(sObject="FlowDefinition", fields=["Id","DeveloperName","NamespacePrefix","MasterLabel","Description","ActiveVersionId","ActiveVersion.VersionNumber","LatestVersionId","LatestVersion.VersionNumber","LatestVersion.Status","LatestVersion.MasterLabel","LatestVersion.Description"])
First get the version Id from the FlowDefinition query above, then:
tooling_api_query(sObject="Flow", fields=["Id","FullName","DefinitionId","Definition.DeveloperName","MasterLabel","Description","VersionNumber","Status","Metadata","ProcessType"], whereClause="Id='<flow version id>'")
Note: do not include FullName or Metadata in multi-record queries — only single-record retrieval supports these.
metadata_create(type="Flow", metadata=[{"fullName": "Flow_Name", "label": "Flow Name", "apiVersion": 65, "processType": "AutoLaunchedFlow", "status": "Draft", ...}])
metadata_read(type="Flow", fullNames=["Flow_Name"])metadata_update(type="Flow", metadata=[{...}], upsert=True)
fullName — version numbers are managed automaticallystatus: Draft and ask user to activate manually if you get an errormetadata_update(type="FlowDefinition", metadata=[{"fullName": "Flow_Name", "activeVersionNumber": <version>}])
To deactivate all versions: set activeVersionNumber to 0.
metadata_update(type="FlowDefinition", metadata=[{"fullName": "Flow_Name", "activeVersionNumber": 0}])tooling_api_dml(operation="delete", sObject="Flow", record={"Id": "<flow version id>"}) (repeat for each version)tooling_api_query(sObject="Flow", fields=["Definition.DeveloperName"], whereClause="Status = 'Active' AND (ProcessType = 'AutolaunchedFlow' OR ProcessType = 'Workflow' OR ProcessType = 'CustomEvent' OR ProcessType = 'InvocableProcess') AND Id NOT IN (SELECT FlowVersionId FROM FlowTestCoverage)")
soql_query(sObject="FlowInterview", fields=["Id","Name","CurrentElement","InterviewStatus","PauseLabel","CreatedDate"], whereClause="InterviewStatus IN ('Paused', 'Failed')")
# Before generating a flow for a custom object
sobject_describe(
sObject="Invoice__c",
sf_user="prod-username"
)
# Returns: Field list, object metadata, standard fields
# Check what flows already exist
metadata_list(
type="Flow",
sf_user="prod-username"
)
# Returns: All Flow metadata objects in org
# Complete example: notify managers when case category changes
metadata_create(
type="Flow",
metadata=[{
"fullName": "Case_Category_Change_Alert",
"apiVersion": 65,
"description": "Sends email when Case Category changes from Billing to Channel",
"environments": ["Default"],
"interviewLabel": "Case Category Change Alert {!$Flow.CurrentDateTime}",
"label": "Case Category Change Alert",
"processMetadataValues": [
{"name": "BuilderType", "value": {"stringValue": "LightningFlowBuilder"}},
{"name": "CanvasMode", "value": {"stringValue": "AUTO_LAYOUT_CANVAS"}}
],
"processType": "AutoLaunchedFlow",
"start": {
"locationX": 0, "locationY": 0,
"connector": {"targetReference": "Check_Previous_Category"},
"filterLogic": "and",
"filters": [
{"field": "Case_Category__c", "operator": "EqualTo", "value": {"stringValue": "Channel"}},
{"field": "Case_Category__c", "operator": "IsChanged", "value": {"booleanValue": True}}
],
"object": "Case",
"recordTriggerType": "Update",
"triggerType": "RecordAfterSave"
},
"decisions": [{
"name": "Check_Previous_Category",
"label": "Check Previous Category",
"locationX": 0, "locationY": 0,
"defaultConnectorLabel": "Default Outcome",
"rules": [{
"name": "Was_Billing",
"conditionLogic": "and",
"conditions": [{
"leftValueReference": "$Record__Prior.Case_Category__c",
"operator": "EqualTo",
"rightValue": {"stringValue": "Billing"}
}],
"connector": {"targetReference": "Send_Email_Action"},
"label": "Was Billing"
}]
}],
"actionCalls": [{
"name": "Send_Email_Action",
"label": "Send Email",
"locationX": 0, "locationY": 0,
"actionName": "emailSimple",
"actionType": "emailSimple",
"flowTransactionModel": "CurrentTransaction",
"inputParameters": [
{"name": "emailAddresses", "value": {"stringValue": "support-managers@example.com"}},
{"name": "emailSubject", "value": {"stringValue": "Case Category Changed to Channel"}},
{"name": "emailBody", "value": {"stringValue": "Case {!$Record.CaseNumber} category changed from Billing to Channel."}}
],
"faultConnector": {"targetReference": "Log_Error"}
}],
"recordCreates": [{
"name": "Log_Error",
"label": "Log Error",
"locationX": 0, "locationY": 0,
"object": "Flow_Error__c",
"inputAssignments": [
{"field": "Flow_Name__c", "value": {"stringValue": "Case_Category_Change_Alert"}},
{"field": "Context_Record_Id__c", "value": {"elementReference": "$Record.Id"}},
{"field": "Error_Source__c", "value": {"stringValue": "Send_Email_Action"}}
]
}],
"status": "Draft"
}],
sf_user="prod-username"
)
# Get the metadata of an existing flow
metadata_read(
type="Flow",
fullNames=["Auto_Lead_Assignment"],
sf_user="prod-username"
)
# Returns: Complete Flow metadata from org (JSON)
# Find all active flows
tooling_api_query(
sObject="FlowDefinition",
fields=["Id", "ApiName", "Description", "Status"],
whereClause="Status = 'Active'",
sf_user="prod-username"
)
| From Skill | To sf-flow | When |
|---|---|---|
| sf-apex | → sf-flow | "Create Flow wrapper for Apex logic" |
| sf-integration | → sf-flow | "Create HTTP Callout Flow" |
| From sf-flow | To Skill | When |
|---|---|---|
| sf-flow | → sf-metadata | "Describe Invoice__c" (verify fields before flow) |
| sf-flow | → sf-data | "Create 200 test Accounts" (after deploy) |
Deployment: See Phase 4 above.
Embed custom Lightning Web Components in Flow Screens for rich, interactive UIs.
| Template | Purpose |
|---|---|
assets/screen-flow-with-lwc.xml | Flow embedding LWC component |
assets/apex-action-template.xml | Flow calling Apex @InvocableMethod |
The XML below shows the structural pattern. When deploying via
metadata_create, translate to the equivalent JSON object.
<screens>
<fields>
<extensionName>c:recordSelector</extensionName>
<fieldType>ComponentInstance</fieldType>
<inputParameters>
<name>recordId</name>
<value><elementReference>var_RecordId</elementReference></value>
</inputParameters>
<outputParameters>
<assignToReference>var_SelectedId</assignToReference>
<name>selectedRecordId</name>
</outputParameters>
</fields>
</screens>
| Resource | Location |
|---|---|
| LWC Integration Guide | references/lwc-integration-guide.md |
| LWC Component Setup | sf-lwc/assets/flow-integration-guide.md |
| Triangle Architecture | references/triangle-pattern.md |
Call Apex @InvocableMethod classes from Flow for complex business logic.
The XML below shows the structural pattern. When deploying via
metadata_create, translate to the equivalent JSON object.
<actionCalls>
<name>Process_Record</name>
<actionName>RecordProcessor</actionName>
<actionType>apex</actionType>
<inputParameters>
<name>recordId</name>
<value><elementReference>var_RecordId</elementReference></value>
</inputParameters>
<outputParameters>
<assignToReference>var_IsSuccess</assignToReference>
<name>isSuccess</name>
</outputParameters>
<faultConnector>
<targetReference>Handle_Error</targetReference>
</faultConnector>
</actionCalls>
| Resource | Location |
|---|---|
| Apex Action Template | assets/apex-action-template.xml |
| Apex @InvocableMethod Guide | sf-apex/references/flow-integration.md |
| Triangle Architecture | references/triangle-pattern.md |
When creating Flows for Agentforce agents:
flow://FlowName targetsVariable Name Matching: When creating Flows for Agentforce agents:
inp_AccountId, out_AccountName)Use out_ prefix for output variables to distinguish them in Action Definition schema:
<variables>
<name>out_CaseSubject</name>
<dataType>String</dataType>
<isOutput>true</isOutput>
</variables>
<variables>
<name>out_CaseStatus</name>
<dataType>String</dataType>
<isOutput>true</isOutput>
</variables>
Flow formulas have more limited function support than formula fields. The table below applies to formula variables and formula elements within the flow, NOT to filterFormula entry conditions:
| Function | In filterFormula (entry conditions) | In flow formulas/variables | Alternative for flow formulas |
|---|---|---|---|
ISNEW() / ISCHANGED() | ✅ Supported | ❌ Not supported | Use $Record__Prior comparisons |
BLANKVALUE() | ✅ Supported | ❌ Not supported | Use Decision element or IF() |
CASESAFEID() | ❌ Not supported | ❌ Not supported | ID variables handle this automatically |
CRITICAL: Creating a Flow is NOT sufficient for Agentforce. The Flow must be registered as an Action Definition.
Registration Workflow:
is_displayable: Can LLM show output to user?is_used_by_planner: Can LLM use output for decisions?Flow Created → Deployed to Org → Action Definition Created → Agent Can Use
↑ ↑ ↑ ↑
sf-flow Cirra AI Setup > Agentforce @actions.MyAction
Why This Matters: The Action Definition is what exposes the Flow to the agent runtime with proper input/output schema mapping. Without it, @actions.FlowName will fail with ValidationError: Tool target 'FlowName' is not an action definition.
| Direction | Pattern |
|---|---|
| sf-flow → sf-metadata | "Describe Invoice__c" (verify fields before flow) |
| sf-flow → Cirra AI | Deploy with validation via metadata_create |
| sf-flow → sf-data | "Create 200 test Accounts" (test data after deploy) |
Dependencies (optional): sf-metadata, sf-data | API: 65.0 | Mode: Strict (warnings block) | MCP Server: Cirra AI (required)
Required Setup:
cirra_ai_init() called once per sessionsf_user parameter--output-dir by defaultValidation hook: Implemented at the plugin level via the PreToolUse hook — pre-mcp-validate.py is dispatched based on tool and metadata type (for example Flow and FlowDefinition) and runs whenever those operations occur, regardless of which skill is active. Use python scripts/validate_flow_cli.py ... for additional on-demand checks.