A judgment at the top of the action the agent already calls
Agentforce agents act through invocable Apex. One governed @InvocableMethod takes the object, the record and the fields, asks Veridect first, and performs the DML only for the greenlit rows — all callouts before any DML, as Apex requires. It returns the verdict and the record id to the agent either way, so the agent can tell the customer the change is waiting for a person instead of pretending it happened. One class covers every object and every agent that uses the action.
Fail-closed by construction: a callout exception, a timeout or a malformed response never reaches the DML branch. The Veridect key lives in a Named Credential, never in code or in the agent’s instructions.
// Apex invocable action "Governed Update" — the only path from an agent to this DML, for any object public with sharing class GovernedUpdate { public class Req { @InvocableVariable public String objectApiName; @InvocableVariable public Id recordId; @InvocableVariable public String fieldsJson; // {"Status":"Escalated","Priority":"High"} @InvocableVariable public String agentName; @InvocableVariable public String agentScope; } public class Res { @InvocableVariable public String verdict; @InvocableVariable public String recordId; @InvocableVariable public Boolean executed; } @InvocableMethod(label='Update Record (governed)' callout=true) public static List<Res> run(List<Req> reqs) { List<Res> out = new List<Res>(); List<SObject> toUpdate = new List<SObject>(); for (Req r : reqs) { // pass 1: every callout, no DML yet (Apex forbids callouts after DML) Res res = new Res(); res.verdict = 'unavailable'; res.executed = false; // closed until proven open Map<String,Object> fields = (Map<String,Object>) JSON.deserializeUntyped(r.fieldsJson); try { HttpRequest h = new HttpRequest(); h.setEndpoint('callout:Veridect/api/ai/consensus/pre-action'); // Named Credential holds the key h.setMethod('POST'); h.setHeader('Content-Type', 'application/json'); h.setTimeout(60000); h.setBody(JSON.serialize(new Map<String,Object>{ 'action' => 'update_' + r.objectApiName.toLowerCase(), 'target' => 'salesforce:' + r.objectApiName.toLowerCase(), 'parameters' => fields, // field names drive the policy classes 'callingAgent' => r.agentName, 'callingAgentScope' => r.agentScope, // its configured authority, in words 'department' => 'customer_service' })); Map<String,Object> d = (Map<String,Object>) JSON.deserializeUntyped(new Http().send(h).getBody()); res.verdict = (String) d.get('verdict'); res.recordId = (String) d.get('auditBundleId'); } catch (Exception e) { /* network, timeout, parse: all stay closed */ } if (res.verdict == 'greenlight') { SObject rec = Schema.getGlobalDescribe().get(r.objectApiName).newSObject(r.recordId); for (String f : fields.keySet()) rec.put(f, fields.get(f)); rec.put('Veridect_Record__c', res.recordId); // the record id travels on the row toUpdate.add(rec); res.executed = true; } out.add(res); } if (!toUpdate.isEmpty()) update toUpdate; // pass 2: one DML, only the greenlit rows return out; } }