Live Org Injection: Bridging AI and Your Actual Salesforce Data
Generic AI advice says "add a lookup field." Org-grounded AI says "add a lookup to Revenue_Forecast__c on Account, which already has 23 custom fields and 3 validation rules that will affect your new relationship." The difference is live org injection.
The Generic Advice Problem
Every AI tool for Salesforce gives you generic advice. "Use a trigger handler pattern." "Enable field history tracking." "Check CRUD permissions." This advice is technically correct and practically useless because it ignores the specific context of your org.
Your org is not generic. It has 247 custom fields on Account. It has 11 validation rules on Opportunity. It has a picklist on Case.Type with 34 values, 8 of which are inactive but still referenced by 2,000+ records. It has an Apex trigger on Contact that fires a Platform Event that triggers a Flow that updates Account. The generic advice does not know any of this.
sf-fabric's live org injection solves this by resolving template variables against your actual Salesforce org at prompt-assembly time. The AI sees your real schema, your real data shapes, your real constraints. It stops giving generic advice and starts giving org-specific recommendations.
The 12 Variable Types
sf-fabric supports 12 types of {{sf:*}} template variables, each resolved by querying the connected Salesforce org:
// From sf-fabric-salesforce/src/template.rs
Supported variables:
{{sf:org_name}} Org alias (from CLI auth)
{{sf:org_type}} Sandbox/Production/Scratch
{{sf:api_version}} API version (e.g., "v62.0")
{{sf:username}} Authenticated username
{{sf:instance_url}} Salesforce instance URL
{{sf:user_id}} Authenticated user/org ID
{{sf:limits}} Org limits summary (API call)
{{sf:metadata:Account}} SObject describe as text
{{sf:soql:SELECT...}} Live SOQL query results
{{sf:tooling:SELECT..}} Tooling API query results
{{sf:apex_classes}} All Apex classes (Tooling)
{{sf:triggers}} All Apex triggers (Tooling)
The first six are lightweight: they come from the authenticated session object and require no additional API calls. The remaining six require live API queries against the org.
The Resolution Pipeline
Variable resolution happens in the Chatter's build_session method, after the system message is assembled from strategy + context + pattern but before it is sent to the LLM. The pipeline has three stages: regex extraction, sequential resolution, and error fallback.
// From sf-fabric-salesforce/src/template.rs
pub async fn resolve_sf_variables(
content: &str, org_alias: &str
) -> Result<String> {
let mut result = content.to_string();
// Stage 1: Simple variables (no API calls)
result = result.replace("{{sf:org_name}}", org_alias);
// Check if any {{sf:...}} tokens remain
let sf_re = Regex::new(r"\{\{sf:([^}]+)\}\}").unwrap();
if !sf_re.is_match(&result) {
return Ok(result);
}
// Stage 2: Connect to org for API-dependent variables
let session = OrgManager::connect(org_alias)?;
result = result.replace(
"{{sf:username}}", &session.username);
result = result.replace(
"{{sf:instance_url}}", &session.instance_url);
// Org type detection from instance URL
if result.contains("{{sf:org_type}}") {
let org_type = if session.instance_url
.contains("test.salesforce.com")
|| session.instance_url.contains(".sandbox.") {
if session.instance_url
.contains("test.salesforce.com") {
"Sandbox"
} else { "Scratch" }
} else {
"Production"
};
result = result.replace("{{sf:org_type}}", org_type);
}
// Stage 3: API-dependent variables with error fallback
// Each variable is resolved independently so one
// failure does not block others
// {{sf:limits}}
if result.contains("{{sf:limits}}") {
let metadata = MetadataClient::new(session.clone());
match metadata.get_org_limits().await {
Ok(limits) => {
let summary = format_limits_summary(&limits);
result = result.replace(
"{{sf:limits}}", &summary);
}
Err(e) => {
result = result.replace(
"{{sf:limits}}",
&format!("(limits unavailable: {e})"));
}
}
}
// {{sf:metadata:ObjectName}} tokens
let metadata_re = Regex::new(
r"\{\{sf:metadata:([^}]+)\}\}"
).unwrap();
let metadata_matches: Vec<(String, String)> = metadata_re
.captures_iter(&result)
.map(|cap| (
cap.get(0).unwrap().as_str().to_string(),
cap[1].to_string()
))
.collect();
if !metadata_matches.is_empty() {
let metadata = MetadataClient::new(session.clone());
for (full_match, object_name) in metadata_matches {
match metadata.describe_as_text(&object_name).await {
Ok(text) => {
result = result.replace(
&full_match, &text);
}
Err(e) => {
result = result.replace(
&full_match,
&format!(
"(failed to describe {}: {})",
object_name, e));
}
}
}
}
// {{sf:soql:SELECT...}} tokens
let soql_re = Regex::new(
r"\{\{sf:soql:([^}]+)\}\}"
).unwrap();
let soql_matches: Vec<(String, String)> = soql_re
.captures_iter(&result)
.map(|cap| (
cap.get(0).unwrap().as_str().to_string(),
cap[1].to_string()
))
.collect();
if !soql_matches.is_empty() {
let soql_client = SoqlClient::new(session);
for (full_match, query) in soql_matches {
// SOQL injection protection
if let Err(e) = validate_soql(&query) {
result = result.replace(
&full_match,
&format!("(SOQL blocked: {e})"));
continue;
}
match soql_client.query_as_text(&query).await {
Ok(text) => {
result = result.replace(
&full_match, &text);
}
Err(e) => {
result = result.replace(
&full_match,
&format!("(SOQL failed: {e})"));
}
}
}
}
Ok(result)
}
The error fallback is critical. If a SOQL query fails or a Describe call times out, the variable is replaced with an error message, not left as a raw {{sf:...}} token. The LLM sees "(SOQL query failed: INVALID_FIELD)" and can report the error to the user instead of trying to interpret a template variable as literal text.
SOQL Injection Protection
The {{sf:soql:...}} variable is powerful and dangerous. A malicious pattern could include {{sf:soql:DELETE FROM Account}} and wipe your data. sf-fabric validates every SOQL query before execution:
const DANGEROUS_SOQL_PATTERNS: &[&str] = &[
"; drop ", "; delete ", "; update ", "; insert ",
";drop ", ";delete ", ";update ", ";insert ",
];
fn validate_soql(query: &str) -> Result<()> {
let lower = query.to_lowercase();
for pattern in DANGEROUS_SOQL_PATTERNS {
if lower.contains(pattern) {
tracing::warn!(
"Blocked dangerous SOQL: {query}"
);
anyhow::bail!(
"SOQL rejected: contains dangerous \
pattern '{pattern}'. This looks like \
a SOQL injection attempt."
);
}
}
Ok(())
}
The validation is intentionally conservative. It blocks any query containing a semicolon followed by a DML keyword. This catches injection attempts like "SELECT Id FROM Account; DELETE FROM Account" while allowing legitimate queries that happen to contain the word "delete" in a string literal (like WHERE Action__c = 'Delete Request').
Additionally, SOQL queries submitted through sf-fabric execute with the authenticated user's permissions. Even if a malicious query somehow bypassed validation, it could only access records the user already has access to. The org's sharing rules, field-level security, and CRUD permissions still apply.
The Authentication Chain
Connecting to a Salesforce org requires an access token. sf-fabric resolves credentials through an authentication chain that checks multiple sources:
Authentication resolution order:
1. SF CLI v2 auth store (~/.sf/)
- Reads from the sf CLI's encrypted auth file
- Works if user has authenticated via `sf org login`
2. SFDX legacy auth store (~/.sfdx/)
- Backward compatibility with sfdx CLI
- Same format, different directory
3. sf-fabric config
- Custom config file with stored credentials
- Uses system keyring for token storage
Resolution:
OrgManager::connect("prod")
-> Check ~/.sf/ for alias "prod"
-> If not found, check ~/.sfdx/ for alias "prod"
-> If not found, check sf-fabric config
-> If not found, error with instructions
The key design decision: sf-fabric does not store credentials itself (except in the system keyring for its own config). It piggybacks on the Salesforce CLI's authentication. If you have authenticated with sf org login web -a prod, sf-fabric can connect to "prod" immediately with no additional setup.
Before and After: The Real Difference
Here is the same pattern (analyze_org_health) run with and without org injection.
Without org injection (generic):
sf-fabric --pattern analyze_org_health \
--message "Assess our Account data model"
AI Output (generic):
Account Data Model Assessment
=============================
The standard Account object typically has:
- Name (required)
- Industry (picklist)
- AnnualRevenue (currency)
- Standard lookups to Contact, Opportunity, Case
Recommendations:
- Ensure required fields are populated
- Review picklist value usage
- Check for duplicate accounts
- Consider implementing account hierarchy
[Generic advice that applies to any org]
With org injection (grounded):
sf-fabric --pattern analyze_org_health \
--strategy governor_aware \
--org prod \
--message "Assess our Account data model"
Pattern system.md includes:
Analyze the schema for {{sf:metadata:Account}}.
Current org limits:
{{sf:limits}}
Active validation rules:
{{sf:tooling:SELECT ValidationName,
ErrorConditionFormula, ErrorMessage
FROM ValidationRule
WHERE EntityDefinition.QualifiedApiName = 'Account'
AND Active = true}}
Recent record statistics:
{{sf:soql:SELECT COUNT(Id) total,
COUNT(Industry) with_industry,
COUNT(AnnualRevenue) with_revenue
FROM Account}}
After resolution, the AI sees:
Schema: Account has 247 fields (189 custom),
23 lookup relationships, key prefix 001.
Fields include: Revenue_Forecast__c (currency),
Customer_Tier__c (picklist: Enterprise, Mid-Market,
SMB, Startup), Renewal_Date__c (date), ...
Org limits: 78% of API calls used today,
storage at 6.2GB of 10GB.
Validation rules:
- RequireBillingAddress: NOT(ISBLANK(BillingStreet))
- RevenueMustBePositive: Revenue_Forecast__c > 0
- TierMatchesRevenue: complex formula...
Statistics: 142,000 total accounts,
138,400 with Industry, 89,200 with Revenue.
AI Output (grounded):
Account Data Model Assessment - prod org
=========================================
CRITICAL: 247 fields on Account (189 custom).
Salesforce limit is 500 but performance degrades
significantly above 200. You are in the danger zone.
Field completeness:
- Industry: 97.5% populated (good)
- AnnualRevenue: 62.8% populated (concerning)
- Revenue_Forecast__c: check population rate
- Customer_Tier__c: verify distribution across tiers
Storage concern: 6.2GB of 10GB used.
Account alone is likely 2-3GB with 247 fields
and 142K records. Consider:
- Archiving fields unused in last 12 months
- Moving large text fields to related object
- Reviewing Revenue_Forecast__c if duplicating
AnnualRevenue data
Validation rule interaction:
The TierMatchesRevenue rule creates a dependency
between Customer_Tier__c and Revenue_Forecast__c.
Any data migration must populate both fields
in the correct order to avoid validation errors.
Governor impact:
With 23 lookup relationships, any trigger on Account
will load significant related data. Ensure triggers
use lazy loading and do not query all relationships
in before-insert context.
[Specific, actionable, grounded in real data]
The difference is not subtle. The generic output is a checklist from a Salesforce study guide. The grounded output is an assessment by someone who has looked at the actual org. It knows about the 247 fields. It knows about the storage pressure. It knows about the validation rule dependency. It knows the exact field population rates.
Token Economics of Org Injection
Variable resolution token costs:
{{sf:org_name}}: ~5 tokens (free, no API call)
{{sf:username}}: ~10 tokens (free, from session)
{{sf:org_type}}: ~5 tokens (free, URL detection)
{{sf:limits}}: ~200 tokens (1 API call)
{{sf:metadata:Account}}: ~800-2000 tokens (1 API call)
{{sf:soql:...}}: ~100-500 tokens (1 API call)
{{sf:tooling:...}}: ~200-1000 tokens (1 API call)
{{sf:apex_classes}}: ~500-3000 tokens (1 API call)
{{sf:triggers}}: ~200-500 tokens (1 API call)
Typical pattern with org injection:
Base pattern: ~800 tokens
{{sf:metadata:Account}}: ~1,500 tokens
{{sf:limits}}: ~200 tokens
{{sf:soql:...}} x 2: ~400 tokens
{{sf:tooling:...}} x 1: ~300 tokens
Strategy: ~200 tokens
User input: ~200 tokens
----------------------------------------
Total input: ~3,600 tokens
Output: ~2,000 tokens
----------------------------------------
Total: ~5,600 tokens
Cost: ~$0.04 per request (Claude Sonnet pricing)
API calls: 4-5 per request
API call latency: ~500ms total (parallel where possible)
The org injection adds roughly 2,000 tokens of input and 500ms of latency (for API calls to Salesforce). In return, it transforms the output from generic to org-specific. That trade-off is worth it every single time.
Patterns That Use Org Injection
Not every pattern needs org injection. Patterns that review pasted code work fine without it. Patterns that analyze, audit, or plan need it. The requires_org metadata flag in pattern.toml tells the CLI whether to require the --org flag:
Patterns requiring org injection (requires_org = true):
analyze_org_health, analyze_permissions,
analyze_field_usage, analyze_data_model,
audit_fls, audit_sharing, audit_crud,
detect_pii, analyze_automation_inventory,
plan_data_migration, validate_migration,
...
Patterns NOT requiring org injection:
review_apex, review_lwc, review_flow,
create_trigger, create_test_class,
create_lwc_component, generate_unit_tests,
...
The split is roughly 40/60. 40% of patterns benefit from live org data. 60% work on user-provided input alone. For the 40%, org injection is not optional. It is the difference between useful and useless output.
The Bridge
Live org injection is the bridge between AI that knows about Salesforce in general and AI that knows about your Salesforce. The template variable system is simple: regex extraction, sequential resolution, error fallback. The authentication chain is simple: check the SF CLI stores first, fall back to config. The injection protection is simple: block dangerous patterns before execution.
Simple mechanisms, dramatic results. The AI stops guessing about your schema and starts using it. It stops recommending generic best practices and starts analyzing your specific situation. It stops hallucinating field names and starts using real ones.
This is not a feature. It is the feature. Everything else in sf-fabric (patterns, strategies, sessions, vendor routing) exists to make this possible: grounding AI in the reality of your actual Salesforce org.