Invocable Apex wrapper for Flow, Apex-trigger orchestration, and Agentforce pre-execution checks.
force-app/main/default/classes/DecionisActionGate.clsThis is the literal Salesforce wrapper layer around the Decionis universal Action Gate and portable Decision Dossier APIs: an invocable Flow action named Evaluate Action with Decionis, a record-side Decision Dossier LWC, a starter permission set, Apex tests, and a deployable package manifest.
Invocable Apex wrapper for Flow, Apex-trigger orchestration, and Agentforce pre-execution checks.
force-app/main/default/classes/DecionisActionGate.clsRecord-side Lightning Web Component that renders outcome, confidence, policy version, signature, and verification link on Salesforce objects.
force-app/main/default/lwc/decionisDecisionDossier/*Permission set that grants access to the Apex wrappers and the Decision Dossier component.
force-app/main/default/permissionsets/DecionisGovernanceStarter.permissionset-meta.xmlPackage manifest for Salesforce DX or unlocked-package deployment of the starter assets.
manifest/package.xmlThe wrapper entry point exposed to Flow and Agentforce. It forwards Salesforce action context to the Decionis Action Gate and maps back decision, dossier, and verification fields.
public with sharing class DecionisActionGate {
public class Request {
@InvocableVariable(required=true)
public String orgId;
@InvocableVariable(required=true)
public String endpointBaseUrl;
@InvocableVariable(required=true)
public String apiKey;
@InvocableVariable(required=true)
public String objectType;
@InvocableVariable(required=true)
public String recordId;
@InvocableVariable
public String recordName;
@InvocableVariable(required=true)
public String actionName;
@InvocableVariable
public String workflowKey;
@InvocableVariable
public String decisionType;
@InvocableVariable
public Decimal amount;
@InvocableVariable
public Decimal riskScore;
@InvocableVariable
public String mode;
@InvocableVariable
public String policyVersion;
@InvocableVariable
public String objectiveProfile;
@InvocableVariable
public String contextJson;
}
public class Response {
@InvocableVariable public String decision;
@InvocableVariable public Boolean continueAction;
@InvocableVariable public Boolean escalateAction;
@InvocableVariable public Boolean delayAction;
@InvocableVariable public String dossierId;
@InvocableVariable public String verificationUrl;
@InvocableVariable public String verificationSignature;
@InvocableVariable public String summary;
@InvocableVariable public String rawResponse;
@InvocableVariable public String errorMessage;
}
private static String asString(Object value) {
return value == null ? null : String.valueOf(value);
}
private static Boolean asBoolean(Object value) {
if (value == null) {
return false;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
return 'true'.equalsIgnoreCase(String.valueOf(value));
}
private static Map<String, Object> parseContext(String contextJson) {
if (String.isBlank(contextJson)) {
return new Map<String, Object>();
}
Object parsed = JSON.deserializeUntyped(contextJson);
if (parsed instanceof Map<String, Object>) {
return (Map<String, Object>) parsed;
}
return new Map<String, Object>();
}
@InvocableMethod(
label='Evaluate Action with Decionis'
description='Calls the Decionis Salesforce Action Gate before a consequential action executes.'
)
public static List<Response> evaluate(List<Request> requests) {
List<Response> responses = new List<Response>();
for (Request request : requests) {
Response responseItem = new Response();
try {
Map<String, Object> payload = new Map<String, Object>{
'object_type' => request.objectType,
'record_id' => request.recordId,
'record_name' => request.recordName,
'action_name' => request.actionName,
'workflow_key' => request.workflowKey,
'decision_type' => request.decisionType,
'amount' => request.amount,
'risk_score' => request.riskScore,
'mode' => request.mode,
'policy_version' => request.policyVersion,
'objective_profile' => request.objectiveProfile,
'context' => parseContext(request.contextJson)
};
DecionisGatewayClient.GatewayResult gatewayResult = DecionisGatewayClient.post(
request.endpointBaseUrl,
'/v1/orgs/' +
EncodingUtil.urlEncode(request.orgId, 'UTF-8') +
'/salesforce/action-gate',
request.apiKey,
payload
);
responseItem.rawResponse = gatewayResult.rawBody;
if (gatewayResult.statusCode >= 300) {
responseItem.errorMessage =
'Decionis action gate returned HTTP ' + String.valueOf(gatewayResult.statusCode);
responses.add(responseItem);
continue;
}
Map<String, Object> body = gatewayResult.body;
Map<String, Object> actionGate = body.containsKey('action_gate') &&
body.get('action_gate') instanceof Map<String, Object>
? (Map<String, Object>) body.get('action_gate')
: new Map<String, Object>();
Map<String, Object> verification = body.containsKey('verification')
&& body.get('verification') instanceof Map<String, Object>
? (Map<String, Object>) body.get('verification')
: new Map<String, Object>();
responseItem.decision = asString(body.get('decision'));
responseItem.continueAction = asBoolean(body.get('continue_action'));
responseItem.escalateAction = asBoolean(body.get('escalate_action'));
responseItem.delayAction = asBoolean(body.get('delay_action'));
responseItem.dossierId = asString(actionGate.get('dossier_id'));
responseItem.verificationUrl = asString(verification.get('verification_page_url'));
responseItem.verificationSignature = asString(verification.get('signature'));
responseItem.summary = asString(body.get('summary'));
} catch (Exception ex) {
responseItem.errorMessage = ex.getMessage();
}
responses.add(responseItem);
}
return responses;
}
}
Starter permission set for wrapper rollout. This gives the integration user the Apex and LWC access needed for the first pilot install.
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<classAccesses>
<apexClass>DecionisActionGate</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>DecionisDecisionDossierController</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>DecionisGatewayClient</apexClass>
<enabled>true</enabled>
</classAccesses>
<fieldPermissions>
<editable>true</editable>
<field>Case.Decionis_Confidence__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Case.Decionis_Dossier_Id__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Case.Decionis_Outcome__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Case.Decionis_Policy_Version__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Case.Decionis_Verify_URL__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Contract.Decionis_Confidence__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Contract.Decionis_Dossier_Id__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Contract.Decionis_Outcome__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Contract.Decionis_Policy_Version__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Contract.Decionis_Verify_URL__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Opportunity.Decionis_Confidence__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Opportunity.Decionis_Dossier_Id__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Opportunity.Decionis_Outcome__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Opportunity.Decionis_Policy_Version__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Opportunity.Decionis_Verify_URL__c</field>
<readable>true</readable>
</fieldPermissions>
<label>Decionis Governance Starter</label>
</PermissionSet>
POST /v1/orgs/:orgId/salesforce/action-gateThin Salesforce wrapper over the universal Action Gate. Returns execute / escalate / delay plus the Decision Dossier component payload.
GET /v1/orgs/:orgId/decision-dossiers/:dossierId/portableCanonical artifact with human-readable lines, machine-readable fields, integrity, verification, distribution history, and channel-ready share-kit templates for Slack, Teams, Jira, email, and record surfaces.
The record-side component that loads the portable Decision Dossier artifact and shows the verification state directly on the record.
import { LightningElement, api, wire } from "lwc";
import { getRecord } from "lightning/uiRecordApi";
import getPortableDossier from "@salesforce/apex/DecionisDecisionDossierController.getPortableDossier";
export default class DecionisDecisionDossier extends LightningElement {
@api recordId;
@api objectApiName;
@api orgId;
@api endpointBaseUrl;
@api apiKey;
@api dossierIdField = "Decionis_Dossier_Id__c";
@api outcomeField = "Decionis_Outcome__c";
@api confidenceField = "Decionis_Confidence__c";
@api policyVersionField = "Decionis_Policy_Version__c";
recordData;
portable;
errorMessage;
isLoading = false;
loadedDossierId;
get recordFields() {
if (!this.objectApiName) {
return [];
}
return [
`${this.objectApiName}.${this.dossierIdField}`,
`${this.objectApiName}.${this.outcomeField}`,
`${this.objectApiName}.${this.confidenceField}`,
`${this.objectApiName}.${this.policyVersionField}`,
];
}
@wire(getRecord, { recordId: "$recordId", optionalFields: "$recordFields" })
wiredRecord({ error, data }) {
if (error) {
this.errorMessage = "Unable to read the Salesforce record.";
return;
}
if (!data) {
return;
}
this.recordData = data;
const dossierId = this.recordFieldValue(this.dossierIdField);
if (dossierId && dossierId !== this.loadedDossierId) {
this.loadPortableDossier(dossierId);
}
}
recordFieldValue(fieldApiName) {
if (!this.recordData || !this.recordData.fields) {
return null;
}
const fieldKey = fieldApiName.includes(".") ? fieldApiName.split(".").pop() : fieldApiName;
const fieldValue = this.recordData.fields[fieldKey];
return fieldValue ? fieldValue.value : null;
}
async loadPortableDossier(dossierId) {
if (!this.orgId || !this.endpointBaseUrl || !this.apiKey) {
this.errorMessage = "Configure orgId, endpointBaseUrl, and apiKey on the component.";
return;
}
this.isLoading = true;
this.errorMessage = null;
try {
this.portable = await getPortableDossier({
orgId: this.orgId,
dossierId,
endpointBaseUrl: this.endpointBaseUrl,
apiKey: this.apiKey,
});
this.loadedDossierId = dossierId;
} catch (error) {
this.errorMessage =
error && error.body && error.body.message
? error.body.message
: "Unable to load the portable Decision Dossier.";
} finally {
this.isLoading = false;
}
}
get hasArtifact() {
return Boolean(this.portable && this.portable.portable);
}
get displayOutcome() {
return (
this.portable?.portable?.machine_readable?.outcome ||
this.recordFieldValue(this.outcomeField) ||
"N/A"
);
}
get displayConfidence() {
return (
this.portable?.portable?.machine_readable?.confidence_percent ||
this.recordFieldValue(this.confidenceField) ||
"N/A"
);
}
get displayPolicyVersion() {
return (
this.portable?.portable?.machine_readable?.policy_version ||
this.recordFieldValue(this.policyVersionField) ||
"N/A"
);
}
get displaySignalsEvaluated() {
const value = this.portable?.portable?.machine_readable?.signals_evaluated;
return value === undefined || value === null ? "N/A" : String(value);
}
get displayVerificationSignature() {
return this.portable?.portable?.verification?.signature || "N/A";
}
handleOpenDossier() {
const url = this.portable?.portable?.links?.dossier_api_url;
if (url) {
window.open(url, "_blank");
}
}
handleVerifyDecision() {
const url = this.portable?.portable?.links?.verification_page_url;
if (url) {
window.open(url, "_blank");
}
}
}