Invocable Apex wrapper for Flow, Apex-trigger orchestration, and Agentforce pre-execution checks.
force-app/main/default/classes/DecionisActionGate.clsInstall the managed package and connect from its Setup tab: you approve once in Salesforce, and your workspace and its key are set up for you. Flows then get Govern Action with Decionis, which needs only the action. This page is also the package source (the Flow actions, the record-side Decision Dossier component, the permission set, Apex tests and a deployable manifest), for teams that deploy it themselves.
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.
// Global so the invocable action and its variables are visible to Flow Builder,
// Agentforce, and Apex in subscriber orgs once installed as a managed package.
global with sharing class DecionisActionGate {
global class Request {
@InvocableVariable(required=true)
global String orgId;
// Either endpointBaseUrl + apiKey, or namedCredential. When namedCredential is set it
// wins: the callout runs as callout:<namedCredential> and the Named Credential's
// external credential supplies auth, so no API key transits Flow configuration.
@InvocableVariable
global String endpointBaseUrl;
@InvocableVariable
global String apiKey;
@InvocableVariable(
label='Named Credential'
description='Name of a Salesforce Named Credential pointing at the Decionis API. Preferred over endpointBaseUrl + apiKey.'
)
global String namedCredential;
@InvocableVariable(required=true)
global String objectType;
@InvocableVariable(required=true)
global String recordId;
@InvocableVariable
global String recordName;
@InvocableVariable(required=true)
global String actionName;
@InvocableVariable
global String workflowKey;
@InvocableVariable
global String decisionType;
@InvocableVariable
global Decimal amount;
@InvocableVariable
global Decimal riskScore;
@InvocableVariable(
label='Mode'
description='SHADOW (observe only), PARALLEL, or ENFORCEMENT. Defaults to the org gate configuration when blank.'
)
global String mode;
@InvocableVariable
global String policyVersion;
@InvocableVariable
global String objectiveProfile;
@InvocableVariable
global String contextJson;
}
global class Response {
@InvocableVariable global String decision;
@InvocableVariable global Boolean continueAction;
@InvocableVariable global Boolean escalateAction;
@InvocableVariable global Boolean delayAction;
@InvocableVariable global String dossierId;
@InvocableVariable global String verificationUrl;
@InvocableVariable global String verificationSignature;
@InvocableVariable global String summary;
@InvocableVariable global String rawResponse;
@InvocableVariable global 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.'
)
global static List<Response> evaluate(List<Request> requests) {
List<Response> responses = new List<Response>();
// Events are accumulated and published once after every callout has completed, so no
// callout ever follows DML inside this transaction.
List<Decionis_Decision__e> decisionEvents = new List<Decionis_Decision__e>();
for (Request request : requests) {
Response responseItem = new Response();
try {
if (String.isBlank(request.namedCredential) && String.isBlank(request.endpointBaseUrl)) {
responseItem.errorMessage =
'Provide either namedCredential or endpointBaseUrl (with apiKey) for the Decionis Action Gate.';
responses.add(responseItem);
continue;
}
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,
request.namedCredential,
'/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'));
decisionEvents.add(
new Decionis_Decision__e(
Record_Id__c = request.recordId,
Object_Type__c = request.objectType,
Action_Name__c = request.actionName,
Outcome__c = responseItem.decision,
Dossier_Id__c = responseItem.dossierId,
Verification_URL__c = responseItem.verificationUrl,
Mode__c = request.mode
)
);
} catch (Exception ex) {
responseItem.errorMessage = ex.getMessage();
}
responses.add(responseItem);
}
if (!decisionEvents.isEmpty()) {
try {
EventBus.publish(decisionEvents);
} catch (Exception publishEx) {
// Best-effort: the governed verdict must reach the Flow even if event
// delivery is unavailable in this org.
}
}
return responses;
}
}
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 namedCredential;
@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) {
// Blank properties use the org's Decionis connection; only a half-set manual
// configuration is refused here.
if (this.endpointBaseUrl && !this.apiKey && !this.namedCredential) {
this.errorMessage =
"Set apiKey with endpointBaseUrl, or leave both blank to use the org's Decionis connection.";
return;
}
this.isLoading = true;
this.errorMessage = null;
try {
this.portable = await getPortableDossier({
orgId: this.orgId,
dossierId,
endpointBaseUrl: this.endpointBaseUrl,
apiKey: this.apiKey,
namedCredential: this.namedCredential,
});
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");
}
}
}
Starter permission set for wrapper rollout. This gives the integration user the Apex and LWC access needed for the first install.
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<applicationVisibilities>
<application>Decionis</application>
<visible>true</visible>
</applicationVisibilities>
<classAccesses>
<apexClass>DecionisActionGate</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>DecionisConnection</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>DecionisDecisionDossierController</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>DecionisGatewayClient</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>DecionisGovernedAction</apexClass>
<enabled>true</enabled>
</classAccesses>
<classAccesses>
<apexClass>DecionisSetupController</apexClass>
<enabled>true</enabled>
</classAccesses>
<externalCredentialPrincipalAccesses>
<enabled>true</enabled>
<externalCredentialPrincipal>decionis__Decionis-Runtime</externalCredentialPrincipal>
</externalCredentialPrincipalAccesses>
<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>
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>false</allowDelete>
<allowEdit>false</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>false</modifyAllRecords>
<object>Decionis_Decision__e</object>
<viewAllRecords>false</viewAllRecords>
</objectPermissions>
<tabSettings>
<tab>Decionis_Setup</tab>
<visibility>Visible</visibility>
</tabSettings>
</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.