Decionis
Salesforce AppExchange

Decionis Salesforce Starter Package

Install 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.

Download Integrity
Source bundle
9ecd01dd0c3b4de5bbf5846d994435704bc3024e5850b95b90280158222fdbc3 decionis-salesforce-starter-package.zip
Mdapi deploy zip
733a9ec741fd8a767f51b1eaca978d69f21c6d6ae6b1bd55369681a3af298c08 decionis-salesforce-starter-package-mdapi.zip
Action Gate

Invocable Apex wrapper for Flow, Apex-trigger orchestration, and Agentforce pre-execution checks.

force-app/main/default/classes/DecionisActionGate.cls
Decision Dossier

Record-side Lightning Web Component that renders outcome, confidence, policy version, signature, and verification link on Salesforce objects.

force-app/main/default/lwc/decionisDecisionDossier/*
Starter Access

Permission set that grants access to the Apex wrappers and the Decision Dossier component.

force-app/main/default/permissionsets/DecionisGovernanceStarter.permissionset-meta.xml
Deployable Manifest

Package manifest for Salesforce DX or unlocked-package deployment of the starter assets.

manifest/package.xml
Installation Flow
Step 1
Install the managed package from decionis.com/install/salesforce (a sandbox installs from /install/salesforce/sandbox).
Step 2
Open the Decionis app. Its Setup tab connects the org: approve once in Salesforce, and your workspace and its key are set up for you.
Step 3
Add Govern Action with Decionis to a Flow before a discount, refund, contract, stage-change or campaign step. New gates start in Shadow Mode.
Step 4
Place the decionisDecisionDossier component on Opportunity, Quote, Contract, Case or custom record pages. In a connected org it needs no configuration.
Step 5
Deploying the source yourself? Use the source bundle or the mdapi zip, assign the Decionis Governance Starter permission set, and set the ApiKey on the Decionis External Credential's Runtime principal.
In a connected org nothing is configured by hand: no endpoint, no key and no org id. The key is least privilege (decision:read and decision:write) and lives in the package's Decionis External Credential. Salesforce stays the workflow surface.
Invocable Apex

The 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;
    }
}
Decision Dossier LWC

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");
    }
  }
}
Permission Set

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>
Runtime Endpoints
Salesforce Action Gate
POST /v1/orgs/:orgId/salesforce/action-gate

Thin Salesforce wrapper over the universal Action Gate. Returns execute / escalate / delay plus the Decision Dossier component payload.

Portable Decision Dossier
GET /v1/orgs/:orgId/decision-dossiers/:dossierId/portable

Canonical 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.